Updated on 2026-08-14

This commit is contained in:
Tangem 2025-01-26 12:50:53 +03:00
parent faafa2724a
commit 04b7287332
45 changed files with 1597 additions and 96 deletions

View file

@ -14,6 +14,7 @@ import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ProductType
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.settings.usercountry.models.UserCountry
import com.tangem.domain.visa.model.VisaCardActivationStatus
import com.tangem.domain.wallets.builder.UserWalletBuilder
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.domain.wallets.models.UserWallet
@ -44,6 +45,12 @@ object OnboardingHelper {
store.state.globalState.onboardingState.onboardingManager ?: OnboardingManager(response)
val cardId = response.card.cardId
return when {
response.cardTypesResolver.isVisaWallet() -> {
if (response.visaCardActivationStatus == null) error("Visa card activation status is null")
response.visaCardActivationStatus !is VisaCardActivationStatus.Activated
}
response.cardTypesResolver.isTangemTwins() -> {
if (!response.twinsIsTwinned()) {
true
@ -68,7 +75,8 @@ object OnboardingHelper {
fun whereToNavigate(scanResponse: ScanResponse): AppRoute {
val newOnboardingSupportTypes = scanResponse.productType == ProductType.Wallet2 ||
scanResponse.productType == ProductType.Ring ||
scanResponse.productType == ProductType.Wallet // AppRoute.OnboardingOther is also supported
scanResponse.productType == ProductType.Wallet ||
scanResponse.productType == ProductType.Visa // AppRoute.OnboardingOther is also supported
if (store.inject(DaggerGraphState::onboardingV2FeatureToggles).isOnboardingV2Enabled &&
newOnboardingSupportTypes
) {

View file

@ -0,0 +1,42 @@
package com.tangem.core.ui.extensions
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.isSimple
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Outline
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.unit.Dp
/**
* Modifier that adds an outset border to the content.
* @see [androidx.compose.foundation.border]
*
* @param width The width of the border.
* @param color The color of the border.
* @param shape The shape of the border.
*/
fun Modifier.outsetBorder(width: Dp, color: Color, shape: Shape) = drawWithCache {
val outline = shape.createOutline(size, layoutDirection, this)
val cornerRadius = when {
outline is Outline.Rounded && outline.roundRect.isSimple -> outline.roundRect.topLeftCornerRadius
else -> CornerRadius.Zero
}
onDrawWithContent {
drawContent()
drawRoundRect(
color = color,
topLeft = Offset(-width.toPx() / 2, -width.toPx() / 2),
size = size.copy(
width = size.width + width.toPx(),
height = size.height + width.toPx(),
),
cornerRadius = cornerRadius,
style = Stroke(width = width.toPx()),
)
}
}

View file

@ -0,0 +1,24 @@
package com.tangem.core.ui.security
import android.view.WindowManager
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.platform.LocalContext
import com.tangem.core.ui.utils.findActivity
/**
* Disables screenshots for the current composition.
*/
@Composable
fun DisableScreenshotsDisposableEffect() {
val activity = LocalContext.current.findActivity()
DisposableEffect(activity) {
activity.window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE,
)
onDispose {
activity.window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
}
}
}

View file

@ -0,0 +1,14 @@
package com.tangem.core.ui.utils
import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
fun Context.findActivity(): Activity {
var context = this
while (context is ContextWrapper) {
if (context is Activity) return context
context = context.baseContext
}
error("Cannot find activity. Please make sure that the context is attached to an activity.")
}

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="8dp" android:viewportHeight="8" android:viewportWidth="32" android:width="32dp">
<path android:fillColor="#0099FF" android:pathData="M28,8C25.791,8 24,6.209 24,4C24,1.791 25.791,0 28,0C30.209,0 32,1.791 32,4C32,6.209 30.209,8 28,8Z"/>
<path android:fillAlpha="0.55" android:fillColor="#0099FF" android:pathData="M15,7C13.343,7 12,5.657 12,4C12,2.343 13.343,1 15,1C16.657,1 18,2.343 18,4C18,5.657 16.657,7 15,7Z" android:strokeAlpha="0.55"/>
<path android:fillAlpha="0.25" android:fillColor="#0099FF" android:pathData="M3,7C1.343,7 0,5.657 0,4C0,2.343 1.343,1 3,1C4.657,1 6,2.343 6,4C6,5.657 4.657,7 3,7Z" android:strokeAlpha="0.25"/>
</vector>

View file

@ -0,0 +1,18 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="315dp"
android:height="198dp"
android:viewportWidth="315"
android:viewportHeight="198">
<path
android:pathData="M10,0L305,0A10,10 0,0 1,315 10L315,188A10,10 0,0 1,305 198L10,198A10,10 0,0 1,0 188L0,10A10,10 0,0 1,10 0z"
android:fillColor="#C9C9C9"/>
<path
android:pathData="M167.85,73.95H147.56C144.35,73.95 142.75,73.95 141.53,74.57C140.45,75.13 139.58,76.01 139.03,77.09C138.41,78.33 138.41,79.94 138.41,83.17V86.66H177V83.17C177,79.94 177,78.33 176.38,77.09C175.83,76.01 174.95,75.13 173.88,74.57C172.66,73.95 171.05,73.95 167.85,73.95Z"
android:fillColor="#4A4A4A"/>
<path
android:pathData="M164.13,97.13H177V113.35C177,116.57 177,118.18 176.37,119.42C175.83,120.5 174.95,121.38 173.88,121.94C172.65,122.56 171.05,122.56 167.85,122.56H164.13V97.13Z"
android:fillColor="#4A4A4A"/>
<path
android:pathData="M151.27,97.13H138.41V113.35C138.41,116.57 138.41,118.18 139.03,119.42C139.58,120.5 140.45,121.38 141.53,121.94C142.75,122.56 144.35,122.56 147.55,122.56H151.27V97.13Z"
android:fillColor="#4A4A4A"/>
</vector>

View file

@ -0,0 +1,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="#1E1E1E" android:fillType="evenOdd" android:pathData="M5,6.25C3.481,6.25 2.25,7.481 2.25,9V18C2.25,19.519 3.481,20.75 5,20.75H19C20.519,20.75 21.75,19.519 21.75,18V9C21.75,7.481 20.519,6.25 19,6.25H5ZM16.5,12.25C15.81,12.25 15.25,12.81 15.25,13.5C15.25,14.19 15.81,14.75 16.5,14.75C17.19,14.75 17.75,14.19 17.75,13.5C17.75,12.81 17.19,12.25 16.5,12.25Z"/>
<path android:fillColor="#1E1E1E" android:pathData="M16.485,3.069C17.754,2.73 19,3.687 19,5.001H9L16.485,3.069Z"/>
</vector>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 786 KiB

Before After
Before After

View file

@ -72,6 +72,7 @@ internal class OnboardingEntryModel @Inject constructor(
ProductType.Visa -> OnboardingRoute.Visa(
scanResponse = scanResponse,
titleProvider = titleProvider,
onDone = ::onVisaOnboardingDone,
)
else -> error("Unsupported")
}
@ -89,6 +90,12 @@ internal class OnboardingEntryModel @Inject constructor(
}
}
private fun onVisaOnboardingDone() {
stackNavigation.navigate {
listOf(OnboardingRoute.Done(onDone = ::navigateToWalletScreen))
}
}
private fun navigateToWalletScreen() {
modelScope.launch(NonCancellable) {
router.replaceAll(AppRoute.Wallet)

View file

@ -31,6 +31,7 @@ internal class OnboardingChildFactory @Inject constructor(
params = OnboardingVisaComponent.Params(
scanResponse = route.scanResponse,
titleProvider = route.titleProvider,
onDone = route.onDone,
),
)
is OnboardingRoute.ManageTokens -> onboardingManageTokensComponentFactory.create(

View file

@ -21,6 +21,7 @@ sealed class OnboardingRoute : Route {
data class Visa(
val titleProvider: TitleProvider,
val scanResponse: ScanResponse,
val onDone: () -> Unit,
) : OnboardingRoute()
data class ManageTokens(

View file

@ -54,7 +54,7 @@ internal class DefaultOnboardingMultiWalletComponent @AssistedInject constructor
private val model: OnboardingMultiWalletModel = getOrCreateModel(params)
private val stackNavigation = StackNavigation<OnboardingMultiWalletState.Step>()
private val artworksState = instanceKeeper.getOrCreateSimple {
private val artworksState = instanceKeeper.getOrCreateSimple(key = "artworksState") {
MutableStateFlow(
when (model.state.value.currentStep) {
CreateWallet -> WalletArtworksState.Folded
@ -68,9 +68,11 @@ internal class DefaultOnboardingMultiWalletComponent @AssistedInject constructor
)
}
private val innerNavigationStateFlow = MutableStateFlow(
MultiWalletInnerNavigationState(1, 5),
)
private val innerNavigationStateFlow = instanceKeeper.getOrCreateSimple(key = "innerNavigationState") {
MutableStateFlow(
MultiWalletInnerNavigationState(1, 5),
)
}
private val childParams = MultiWalletChildParams(
multiWalletState = model.state,

View file

@ -1,19 +1,14 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase
import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import android.view.WindowManager
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.security.DisableScreenshotsDisposableEffect
import com.tangem.features.onboarding.v2.impl.R
import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildComponent
import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams
@ -79,30 +74,11 @@ internal class MultiWalletSeedPhraseComponent(
model.onBack()
}
// disable screenshots
val activity = LocalContext.current.findActivity()
DisposableEffect(activity) {
activity.window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE,
)
onDispose {
activity.window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
}
}
DisableScreenshotsDisposableEffect()
MultiWalletSeedPhrase(
modifier = modifier,
state = state,
)
}
private fun Context.findActivity(): Activity {
var context = this
while (context is ContextWrapper) {
if (context is Activity) return context
context = context.baseContext
}
error("")
}
}

View file

@ -11,6 +11,7 @@ interface OnboardingVisaComponent : ComposableContentComponent, InnerNavigationH
data class Params(
val scanResponse: ScanResponse,
val titleProvider: TitleProvider,
val onDone: () -> Unit,
)
interface Factory : ComponentFactory<Params, OnboardingVisaComponent>

View file

@ -1,15 +1,34 @@
package com.tangem.features.onboarding.v2.visa.impl
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import com.arkivanov.decompose.extensions.compose.jetpack.stack.Children
import com.arkivanov.decompose.extensions.compose.jetpack.stack.animation.slide
import com.arkivanov.decompose.extensions.compose.jetpack.stack.animation.stackAnimation
import com.arkivanov.decompose.extensions.compose.jetpack.subscribeAsState
import com.arkivanov.decompose.router.stack.*
import com.arkivanov.decompose.value.Value
import com.arkivanov.essenty.instancekeeper.getOrCreateSimple
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.decompose.navigation.inner.InnerNavigation
import com.tangem.core.decompose.navigation.inner.InnerNavigationState
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.onboarding.v2.visa.api.OnboardingVisaComponent
import com.tangem.features.onboarding.v2.visa.impl.child.accesscode.OnboardingVisaAccessCodeComponent
import com.tangem.features.onboarding.v2.visa.impl.child.approve.OnboardingVisaApproveComponent
import com.tangem.features.onboarding.v2.visa.impl.child.choosewallet.OnboardingVisaChooseWalletComponent
import com.tangem.features.onboarding.v2.visa.impl.child.inprogress.OnboardingVisaInProgressComponent
import com.tangem.features.onboarding.v2.visa.impl.child.otherwallet.OnboardingVisaOtherWalletComponent
import com.tangem.features.onboarding.v2.visa.impl.child.pincode.OnboardingVisaPinCodeComponent
import com.tangem.features.onboarding.v2.visa.impl.child.welcome.OnboardingVisaWelcomeComponent
import com.tangem.features.onboarding.v2.visa.impl.model.OnboardingVisaModel
import com.tangem.features.onboarding.v2.visa.impl.route.OnboardingVisaRoute
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@ -21,27 +40,144 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor(
@Assisted private val params: OnboardingVisaComponent.Params,
) : OnboardingVisaComponent, AppComponentContext by appComponentContext {
private val innerNavigationState = MutableStateFlow(
OnboardingVisaInnerNavigationState(
stackSize = 1,
stackMaxSize = 7,
),
)
private val model: OnboardingVisaModel = getOrCreateModel(params)
private val currentChildBackEventHandle = MutableSharedFlow<Unit>(onBufferOverflow = BufferOverflow.DROP_LATEST)
private val innerNavigationState = instanceKeeper.getOrCreateSimple(key = "innerNavigationState") {
MutableStateFlow(
OnboardingVisaInnerNavigationState(
stackSize = 1,
stackMaxSize = 7,
),
)
}
private val currentChildBackEventHandle = instanceKeeper.getOrCreateSimple(key = "currentChildBackEventHandle") {
MutableSharedFlow<Unit>()
}
private val stackNavigation = StackNavigation<OnboardingVisaRoute>()
private val childStack: Value<ChildStack<OnboardingVisaRoute, ComposableContentComponent>> =
childStack(
key = "innerStack",
source = stackNavigation,
serializer = null,
initialConfiguration = model.initialRoute,
handleBackButton = true,
childFactory = { configuration, factoryContext ->
createChild(
configuration,
childByContext(factoryContext),
)
},
)
override val innerNavigation: InnerNavigation = object : InnerNavigation {
override val state: StateFlow<InnerNavigationState> = innerNavigationState
override fun pop(onComplete: (Boolean) -> Unit) {
// TODO add stack handling
componentScope.launch { currentChildBackEventHandle.emit(Unit) }
if (childStack.value.active.configuration is OnboardingVisaRoute.AccessCode) {
componentScope.launch { currentChildBackEventHandle.emit(Unit) }
} else {
stackNavigation.pop()
}
}
}
private fun createChild(
route: OnboardingVisaRoute,
factoryContext: AppComponentContext,
): ComposableContentComponent {
return when (route) {
is OnboardingVisaRoute.Welcome -> OnboardingVisaWelcomeComponent(
appComponentContext = factoryContext,
params = OnboardingVisaWelcomeComponent.Params(
isWelcomeBack = route.isWelcomeBack,
onBack = ::onChildBack,
onDone = { stackNavigation.push(OnboardingVisaRoute.AccessCode) },
),
)
OnboardingVisaRoute.AccessCode -> OnboardingVisaAccessCodeComponent(
appComponentContext = factoryContext,
params = OnboardingVisaAccessCodeComponent.Params(
onBack = ::onChildBack,
parentBackEvent = currentChildBackEventHandle,
onDone = { stackNavigation.push(OnboardingVisaRoute.ChooseWallet) },
),
)
OnboardingVisaRoute.ChooseWallet -> OnboardingVisaChooseWalletComponent(
appComponentContext = factoryContext,
params = OnboardingVisaChooseWalletComponent.Params(
onBack = ::onChildBack,
onEvent = { event ->
stackNavigation.push(
when (event) {
OnboardingVisaChooseWalletComponent.Params.Event.TangemWallet ->
OnboardingVisaRoute.TangemWalletApproveOption
OnboardingVisaChooseWalletComponent.Params.Event.OtherWallet ->
OnboardingVisaRoute.OtherWalletApproveOption
},
)
},
),
)
OnboardingVisaRoute.InProgress -> OnboardingVisaInProgressComponent(
appComponentContext = factoryContext,
params = OnboardingVisaInProgressComponent.Params(
onBack = ::onChildBack,
onDone = { stackNavigation.push(OnboardingVisaRoute.PinCode) },
),
)
OnboardingVisaRoute.OtherWalletApproveOption -> OnboardingVisaOtherWalletComponent(
appComponentContext = factoryContext,
params = OnboardingVisaOtherWalletComponent.Params(
onBack = ::onChildBack,
onDone = { stackNavigation.push(OnboardingVisaRoute.InProgress) },
),
)
OnboardingVisaRoute.PinCode -> OnboardingVisaPinCodeComponent(
appComponentContext = factoryContext,
params = OnboardingVisaPinCodeComponent.Params(
onBack = ::onChildBack,
onDone = { params.onDone() },
),
)
OnboardingVisaRoute.TangemWalletApproveOption -> OnboardingVisaApproveComponent(
appComponentContext = factoryContext,
params = OnboardingVisaApproveComponent.Params(
onBack = ::onChildBack,
onDone = { stackNavigation.push(OnboardingVisaRoute.InProgress) },
),
)
}
}
private fun onChildBack() {
when (childStack.value.active.configuration) {
OnboardingVisaRoute.AccessCode,
OnboardingVisaRoute.TangemWalletApproveOption,
OnboardingVisaRoute.OtherWalletApproveOption,
-> stackNavigation.pop()
is OnboardingVisaRoute.Welcome,
OnboardingVisaRoute.ChooseWallet,
OnboardingVisaRoute.InProgress,
OnboardingVisaRoute.OtherWalletApproveOption,
OnboardingVisaRoute.PinCode,
-> {
}
}
}
@Composable
override fun Content(modifier: Modifier) {
TODO("Not yet implemented")
val stackState by childStack.subscribeAsState()
Children(
stack = stackState,
animation = stackAnimation(slide()),
) {
it.instance.Content(modifier)
}
}
@AssistedFactory

View file

@ -8,6 +8,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.core.ui.security.DisableScreenshotsDisposableEffect
import com.tangem.features.onboarding.v2.visa.impl.child.accesscode.model.OnboardingVisaAccessCodeModel
import com.tangem.features.onboarding.v2.visa.impl.child.accesscode.ui.OnboardingVisaAccessCode
import kotlinx.coroutines.flow.SharedFlow
@ -29,6 +30,9 @@ internal class OnboardingVisaAccessCodeComponent(
model.onBack()
}
}
componentScope.launch {
model.onDone.collect { params.onDone() }
}
}
@Composable
@ -37,11 +41,14 @@ internal class OnboardingVisaAccessCodeComponent(
BackHandler(onBack = model::onBack)
DisableScreenshotsDisposableEffect()
OnboardingVisaAccessCode(state, modifier)
}
data class Params(
val parentBackEvent: SharedFlow<Unit>,
val onBack: () -> Unit,
val onDone: () -> Unit,
)
}

View file

@ -1,6 +1,7 @@
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.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.model.Model
import com.tangem.features.onboarding.v2.visa.impl.child.accesscode.ui.state.OnboardingVisaAccessCodeUM
@ -14,12 +15,14 @@ import javax.inject.Inject
@ComponentScoped
internal class OnboardingVisaAccessCodeModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
@Suppress("UnusedPrivateMember")
private val tangemSdkManager: TangemSdkManager,
) : Model() {
private val _uiState = MutableStateFlow(getInitialState())
val uiState = _uiState.asStateFlow()
val onBack = MutableSharedFlow<Unit>()
val onDone = MutableSharedFlow<Unit>()
fun onBack() {
when (uiState.value.step) {
@ -31,64 +34,90 @@ internal class OnboardingVisaAccessCodeModel @Inject constructor(
private fun getInitialState(): OnboardingVisaAccessCodeUM {
return OnboardingVisaAccessCodeUM(
onAccessCodeFirstChange = { _uiState.update { it.copy(accessCodeFirst = it.accessCodeFirst) } },
onAccessCodeSecondChange = { _uiState.update { it.copy(accessCodeSecond = it.accessCodeSecond) } },
onAccessCodeFirstChange = ::onAccessCodeFirstChange,
onAccessCodeSecondChange = ::onAccessCodeSecondChange,
onAccessCodeHideClick = { _uiState.update { it.copy(accessCodeHidden = !it.accessCodeHidden) } },
onContinue = { onContinue() },
)
}
private fun onContinue() {
if (checkAccessCode().not()) return
private fun onAccessCodeFirstChange(textFieldValue: TextFieldValue) {
_uiState.update {
it.copy(
accessCodeFirst = textFieldValue,
atLeastMinCharsError = false,
)
}
}
private fun onAccessCodeSecondChange(textFieldValue: TextFieldValue) {
_uiState.update {
it.copy(
accessCodeSecond = textFieldValue,
codesNotMatchError = false,
)
}
}
private fun onContinue() {
when (uiState.value.step) {
OnboardingVisaAccessCodeUM.Step.Enter -> {
if (checkAccessCodeMinChars().not()) return
_uiState.update { it.copy(step = OnboardingVisaAccessCodeUM.Step.ReEnter) }
}
OnboardingVisaAccessCodeUM.Step.ReEnter -> {
if (checkAccessCodesMatch().not()) return
startActivationProcess(accessCode = uiState.value.accessCodeFirst.text)
}
}
}
private fun checkAccessCode(): Boolean {
private fun checkAccessCodeMinChars(): Boolean {
val first = uiState.value.accessCodeFirst.text
val second = uiState.value.accessCodeSecond.text
when {
first.length < ACCESS_CODE_MIN_LENGTH -> {
_uiState.update { it.copy(atLeastMinCharsError = true) }
return false
}
first != second -> {
_uiState.update { it.copy(codesNotMatchError = true) }
return false
}
if (first.length < ACCESS_CODE_MIN_LENGTH) {
_uiState.update { it.copy(atLeastMinCharsError = true) }
return false
}
return true
}
private fun startActivationProcess(accessCode: String) {
modelScope.launch {
@Suppress("UnusedPrivateMember")
val result = tangemSdkManager.activateVisaCard(
accessCode = accessCode,
challengeToSign = null,
activationInput = TODO(),
)
private fun checkAccessCodesMatch(): Boolean {
val first = uiState.value.accessCodeFirst.text
val second = uiState.value.accessCodeSecond.text
// when (result) {
// is CompletionResult.Success -> {
// val response = result.data
// TODO()
// }
// is CompletionResult.Failure -> {
// // show alert
// TODO()
// }
// }
if (first != second) {
_uiState.update { it.copy(codesNotMatchError = true) }
return false
}
return true
}
@Suppress("UnusedPrivateMember")
private fun startActivationProcess(accessCode: String) {
modelScope.launch { onDone.emit(Unit) }
// 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()
// }
// }
// }
}
private companion object {

View file

@ -43,7 +43,7 @@ internal fun OnboardingVisaAccessCode(state: OnboardingVisaAccessCodeUM, modifie
.weight(1f),
targetState = state.step,
transitionSpec = TangemAnimations.AnimatedContent
.slide { initial, target -> initial.ordinal < target.ordinal },
.slide { initial, target -> initial.ordinal > target.ordinal },
) { step ->
Content(
state = state,

View file

@ -2,7 +2,7 @@ package com.tangem.features.onboarding.v2.visa.impl.child.accesscode.ui.state
import androidx.compose.ui.text.input.TextFieldValue
data class OnboardingVisaAccessCodeUM(
internal data class OnboardingVisaAccessCodeUM(
val step: Step = Step.Enter,
val accessCodeFirst: TextFieldValue = TextFieldValue(""),
val accessCodeSecond: TextFieldValue = TextFieldValue(""),

View file

@ -0,0 +1,45 @@
package com.tangem.features.onboarding.v2.visa.impl.child.approve
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.onboarding.v2.visa.impl.child.approve.model.OnboardingVisaApproveModel
import com.tangem.features.onboarding.v2.visa.impl.child.approve.ui.OnboardingVisaApprove
import kotlinx.coroutines.launch
internal class OnboardingVisaApproveComponent(
appComponentContext: AppComponentContext,
private val params: Params,
) : ComposableContentComponent, AppComponentContext by appComponentContext {
private val model: OnboardingVisaApproveModel = getOrCreateModel()
init {
componentScope.launch {
model.onDone.collect { params.onDone() }
}
}
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
BackHandler(onBack = remember(this) { { params.onBack() } })
OnboardingVisaApprove(
state = state,
modifier = modifier,
)
}
data class Params(
val onBack: () -> Unit,
val onDone: () -> Unit,
)
}

View file

@ -0,0 +1,35 @@
package com.tangem.features.onboarding.v2.visa.impl.child.approve.model
import androidx.compose.runtime.Stable
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.model.Model
import com.tangem.features.onboarding.v2.visa.impl.child.approve.ui.state.OnboardingVisaApproveUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@Stable
@ComponentScoped
internal class OnboardingVisaApproveModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
) : Model() {
private val _uiState = MutableStateFlow(getInitialState())
val uiState = _uiState.asStateFlow()
val onDone = MutableSharedFlow<Unit>()
private fun getInitialState(): OnboardingVisaApproveUM {
return OnboardingVisaApproveUM(
onApproveClick = ::onApproveClick,
)
}
private fun onApproveClick() {
// TODO
modelScope.launch { onDone.emit(Unit) }
}
}

View file

@ -0,0 +1,83 @@
package com.tangem.features.onboarding.v2.visa.impl.child.approve.ui
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
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.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.approve.ui.state.OnboardingVisaApproveUM
@Composable
internal fun OnboardingVisaApprove(state: OnboardingVisaApproveUM, modifier: Modifier = Modifier) {
Column(
modifier
.fillMaxSize()
.navigationBarsPadding(),
) {
Column(
modifier = Modifier
.weight(1f)
.padding(top = 56.dp, bottom = 32.dp)
.padding(horizontal = 34.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Image(
imageVector = ImageVector.vectorResource(R.drawable.ic_tangem_card_grey),
contentDescription = null,
modifier = Modifier.fillMaxWidth(),
)
SpacerH(86.dp)
Text(
text = "Prepare Tangem Wallet",
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
)
SpacerH(12.dp)
Text(
text = "Prepare the Tangem card\nand tap to approve",
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
)
}
NavigationPrimaryButton(
modifier = Modifier
.imePadding()
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
.fillMaxWidth(),
primaryButton = NavigationButton(
textReference = TextReference.Str("Approve"),
iconRes = R.drawable.ic_tangem_24,
isIconVisible = true,
onClick = state.onApproveClick,
),
)
}
}
@Preview(showBackground = true)
@Composable
private fun Preview() {
TangemThemePreview {
OnboardingVisaApprove(state = OnboardingVisaApproveUM())
}
}

View file

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

View file

@ -0,0 +1,50 @@
package com.tangem.features.onboarding.v2.visa.impl.child.choosewallet
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.onboarding.v2.visa.impl.child.choosewallet.model.OnboardingVisaChooseWalletModel
import com.tangem.features.onboarding.v2.visa.impl.child.choosewallet.ui.OnboardingVisaChooseWallet
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
internal class OnboardingVisaChooseWalletComponent(
appComponentContext: AppComponentContext,
private val params: Params,
) : ComposableContentComponent, AppComponentContext by appComponentContext {
private val model: OnboardingVisaChooseWalletModel = getOrCreateModel()
init {
model.onEvent
.onEach { params.onEvent(it) }
.launchIn(componentScope)
}
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
BackHandler(onBack = remember(this) { { params.onBack() } })
OnboardingVisaChooseWallet(
modifier = modifier,
state = state,
)
}
data class Params(
val onBack: () -> Unit,
val onEvent: (Event) -> Unit,
) {
enum class Event {
TangemWallet, OtherWallet
}
}
}

View file

@ -0,0 +1,59 @@
package com.tangem.features.onboarding.v2.visa.impl.child.choosewallet.model
import androidx.compose.runtime.Stable
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.ui.extensions.TextReference
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
import com.tangem.features.onboarding.v2.visa.impl.child.choosewallet.ui.state.SelectableChainRowUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.persistentListOf
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 OnboardingVisaChooseWalletModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
) : Model() {
private val _uiState = MutableStateFlow(getInitialState())
val onEvent = MutableSharedFlow<Event>()
val uiState = _uiState.asStateFlow()
private fun getInitialState(): OnboardingVisaChooseWalletUM {
val selectedOption = SelectableChainRowUM(
event = Event.TangemWallet,
icon = R.drawable.ic_tangem_24,
text = TextReference.Str("Tangem Wallet"),
)
return OnboardingVisaChooseWalletUM(
options = persistentListOf(
selectedOption,
SelectableChainRowUM(
event = Event.OtherWallet,
icon = R.drawable.ic_wallet_filled_24,
text = TextReference.Str("Other Wallet"),
),
),
selectedOption = selectedOption,
onOptionSelected = { option -> _uiState.update { it.copy(selectedOption = option) } },
onContinueClick = ::onContinueClicked,
)
}
private fun onContinueClicked() {
modelScope.launch {
val selectedOption = _uiState.value.selectedOption ?: return@launch
onEvent.emit(selectedOption.event)
}
}
}

View file

@ -0,0 +1,174 @@
package com.tangem.features.onboarding.v2.visa.impl.child.choosewallet.ui
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.components.notifications.NotificationConfig
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.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.choosewallet.OnboardingVisaChooseWalletComponent
import com.tangem.features.onboarding.v2.visa.impl.child.choosewallet.ui.state.OnboardingVisaChooseWalletUM
import com.tangem.features.onboarding.v2.visa.impl.child.choosewallet.ui.state.SelectableChainRowUM
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@Composable
internal fun OnboardingVisaChooseWallet(state: OnboardingVisaChooseWalletUM, modifier: Modifier = Modifier) {
Column(
modifier
.fillMaxSize()
.navigationBarsPadding(),
) {
Column(
Modifier
.weight(1f)
.padding(top = 12.dp)
.padding(horizontal = 16.dp),
) {
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.",
),
iconResId = R.drawable.ic_alert_circle_24,
),
iconTint = TangemTheme.colors.icon.accent,
)
Text(
modifier = Modifier
.padding(
top = 12.dp,
bottom = 8.dp,
start = 12.dp,
end = 12.dp,
),
text = "Choose wallet",
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
)
SelectableChainRows(
items = state.options,
selected = state.selectedOption,
onClick = state.onOptionSelected,
)
}
PrimaryButton(
modifier = Modifier
.imePadding()
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
.fillMaxWidth(),
text = "Continue",
onClick = state.onContinueClick,
)
}
}
@Composable
private fun SelectableChainRows(
items: ImmutableList<SelectableChainRowUM>,
selected: SelectableChainRowUM?,
onClick: (SelectableChainRowUM) -> Unit,
modifier: Modifier = Modifier,
) {
Column(modifier) {
items.forEach { item ->
SelectableChainRow(
state = item,
selected = item == selected,
onClick = { onClick(item) },
)
}
}
}
@Composable
private fun SelectableChainRow(
state: SelectableChainRowUM,
selected: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
RowContentContainer(
modifier = modifier
.heightIn(min = 48.dp)
.outsetBorder(
color = if (selected) TangemTheme.colors.icon.accent.copy(alpha = 0.15f) else Color.Transparent,
width = 5.dp,
shape = RoundedCornerShape(size = 18.dp),
)
.clip(TangemTheme.shapes.roundedCornersXMedium)
.border(
width = 1.dp,
color = if (selected) TangemTheme.colors.icon.accent else Color.Transparent,
shape = TangemTheme.shapes.roundedCornersXMedium,
)
.clickable(onClick = onClick)
.padding(12.dp),
icon = {
Icon(
imageVector = ImageVector.vectorResource(id = state.icon),
tint = if (selected) TangemTheme.colors.icon.primary1 else TangemTheme.colors.icon.informative,
contentDescription = null,
)
},
text = {
Text(
text = state.text.resolveReference(),
style = TangemTheme.typography.button,
color = if (selected) TangemTheme.colors.text.primary1 else TangemTheme.colors.text.secondary,
)
},
action = {},
horizontalArrangement = Arrangement.spacedBy(12.dp),
)
}
@Preview(showBackground = true)
@Composable
private fun Preview() {
TangemThemePreview {
OnboardingVisaChooseWallet(
state = OnboardingVisaChooseWalletUM(
options = persistentListOf(
SelectableChainRowUM(
event = OnboardingVisaChooseWalletComponent.Params.Event.TangemWallet,
icon = R.drawable.ic_tangem_24,
text = TextReference.Str("Tangem Wallet"),
),
SelectableChainRowUM(
event = OnboardingVisaChooseWalletComponent.Params.Event.OtherWallet,
icon = R.drawable.ic_wallet_filled_24,
text = TextReference.Str("Other Wallet"),
),
),
selectedOption = SelectableChainRowUM(
event = OnboardingVisaChooseWalletComponent.Params.Event.OtherWallet,
icon = R.drawable.ic_tangem_24,
text = TextReference.Str("Tangem Wallet"),
),
),
)
}
}

View file

@ -0,0 +1,11 @@
package com.tangem.features.onboarding.v2.visa.impl.child.choosewallet.ui.state
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
internal data class OnboardingVisaChooseWalletUM(
val options: ImmutableList<SelectableChainRowUM> = persistentListOf(),
val selectedOption: SelectableChainRowUM? = null,
val onOptionSelected: (SelectableChainRowUM) -> Unit = {},
val onContinueClick: () -> Unit = {},
)

View file

@ -0,0 +1,11 @@
package com.tangem.features.onboarding.v2.visa.impl.child.choosewallet.ui.state
import androidx.annotation.DrawableRes
import com.tangem.core.ui.extensions.TextReference
import com.tangem.features.onboarding.v2.visa.impl.child.choosewallet.OnboardingVisaChooseWalletComponent
internal data class SelectableChainRowUM(
val event: OnboardingVisaChooseWalletComponent.Params.Event,
@DrawableRes val icon: Int,
val text: TextReference,
)

View file

@ -0,0 +1,41 @@
package com.tangem.features.onboarding.v2.visa.impl.child.inprogress
import androidx.activity.compose.BackHandler
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.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.onboarding.v2.visa.impl.child.inprogress.model.OnboardingVisaInProgressModel
import com.tangem.features.onboarding.v2.visa.impl.child.inprogress.ui.OnboardingVisaInProgress
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
class OnboardingVisaInProgressComponent(
appComponentContext: AppComponentContext,
private val params: Params,
) : ComposableContentComponent, AppComponentContext by appComponentContext {
private val model: OnboardingVisaInProgressModel = getOrCreateModel()
init {
model.onDone
.onEach { params.onDone() }
.launchIn(componentScope)
}
@Composable
override fun Content(modifier: Modifier) {
BackHandler(onBack = remember(this) { { params.onBack() } })
OnboardingVisaInProgress(
modifier = modifier,
)
}
data class Params(
val onBack: () -> Unit,
val onDone: () -> Unit,
)
}

View file

@ -0,0 +1,27 @@
package com.tangem.features.onboarding.v2.visa.impl.child.inprogress.model
import androidx.compose.runtime.Stable
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.model.Model
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@Stable
@ComponentScoped
internal class OnboardingVisaInProgressModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
) : Model() {
val onDone = MutableSharedFlow<Unit>()
init {
modelScope.launch {
// TODO check state
delay(timeMillis = 2000)
onDone.emit(Unit)
}
}
}

View file

@ -0,0 +1,37 @@
package com.tangem.features.onboarding.v2.visa.impl.child.inprogress.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.res.TangemThemePreview
@Composable
internal fun OnboardingVisaInProgress(modifier: Modifier = Modifier) {
Column(
modifier
.fillMaxSize()
.navigationBarsPadding(),
) {
Column(
Modifier
.weight(1f)
.padding(top = 12.dp)
.padding(horizontal = 16.dp),
) {
// TODO
}
}
}
@Preview(showBackground = true)
@Composable
private fun Preview() {
TangemThemePreview {
OnboardingVisaInProgress()
}
}

View file

@ -0,0 +1,46 @@
package com.tangem.features.onboarding.v2.visa.impl.child.otherwallet
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.onboarding.v2.visa.impl.child.otherwallet.model.OnboardingVisaOtherWalletModel
import com.tangem.features.onboarding.v2.visa.impl.child.otherwallet.ui.OnboardingVisaOtherWallet
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
internal class OnboardingVisaOtherWalletComponent(
appComponentContext: AppComponentContext,
private val params: Params,
) : ComposableContentComponent, AppComponentContext by appComponentContext {
private val model: OnboardingVisaOtherWalletModel = getOrCreateModel()
init {
model.onDone
.onEach { params.onDone() }
.launchIn(componentScope)
}
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
BackHandler(onBack = remember(this) { { params.onBack() } })
OnboardingVisaOtherWallet(
modifier = modifier,
state = state,
)
}
data class Params(
val onBack: () -> Unit,
val onDone: () -> Unit,
)
}

View file

@ -0,0 +1,38 @@
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.features.onboarding.v2.visa.impl.child.otherwallet.ui.state.OnboardingVisaOtherWalletUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import javax.inject.Inject
@Stable
@ComponentScoped
internal class OnboardingVisaOtherWalletModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
) : Model() {
private val _uiState = MutableStateFlow(getInitialState())
val uiState = _uiState.asStateFlow()
val onDone = MutableSharedFlow<Unit>()
private fun getInitialState(): OnboardingVisaOtherWalletUM {
return OnboardingVisaOtherWalletUM(
onShareClick = ::onShareClicked,
onOpenInBrowserClick = ::onOpenInBrowserClicked,
)
}
private fun onShareClicked() {
// TODO
}
private fun onOpenInBrowserClicked() {
// TODO
}
}

View file

@ -0,0 +1,137 @@
package com.tangem.features.onboarding.v2.visa.impl.child.otherwallet.ui
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
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.PrimaryButton
import com.tangem.core.ui.components.SecondaryButton
import com.tangem.core.ui.components.SpacerH12
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.otherwallet.ui.state.OnboardingVisaOtherWalletUM
@Composable
internal fun OnboardingVisaOtherWallet(state: OnboardingVisaOtherWalletUM, modifier: Modifier = Modifier) {
Column(
modifier
.fillMaxSize()
.navigationBarsPadding(),
) {
Box(
modifier = Modifier
.weight(1f)
.padding(top = 12.dp, bottom = 32.dp)
.padding(horizontal = 16.dp),
contentAlignment = Alignment.Center,
) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
IconBlock()
SpacerH12()
Text(
text = "Go to Website",
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
)
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",
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
)
}
}
Column {
PrimaryButton(
modifier = Modifier
.padding(horizontal = 16.dp)
.fillMaxWidth(),
text = "Open in Browser",
onClick = state.onOpenInBrowserClick,
)
SpacerH12()
SecondaryButton(
modifier = Modifier
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
.fillMaxWidth(),
text = "Share Link",
onClick = state.onOpenInBrowserClick,
)
}
}
}
@Composable
private fun IconBlock(modifier: Modifier = Modifier) {
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
Box(
modifier = Modifier
.size(64.dp)
.background(TangemTheme.colors.button.primary, TangemTheme.shapes.roundedCorners8),
contentAlignment = Alignment.Center,
) {
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.ic_tangem_24),
contentDescription = null,
tint = TangemTheme.colors.icon.primary2,
modifier = Modifier.size(38.dp),
)
}
Image(
imageVector = ImageVector.vectorResource(id = R.drawable.ic_loader),
contentDescription = null,
modifier = Modifier.width(32.dp),
)
Box(
modifier = Modifier
.size(64.dp)
.background(TangemTheme.colors.background.tertiary, TangemTheme.shapes.roundedCorners8),
contentAlignment = Alignment.Center,
) {
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.ic_web_24),
contentDescription = null,
tint = TangemTheme.colors.icon.primary1,
modifier = Modifier.size(48.dp),
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun Preview() {
TangemThemePreview {
OnboardingVisaOtherWallet(OnboardingVisaOtherWalletUM())
}
}

View file

@ -0,0 +1,6 @@
package com.tangem.features.onboarding.v2.visa.impl.child.otherwallet.ui.state
internal data class OnboardingVisaOtherWalletUM(
val onShareClick: () -> Unit = {},
val onOpenInBrowserClick: () -> Unit = {},
)

View file

@ -0,0 +1,48 @@
package com.tangem.features.onboarding.v2.visa.impl.child.pincode
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.security.DisableScreenshotsDisposableEffect
import com.tangem.features.onboarding.v2.visa.impl.child.pincode.model.OnboardingVisaPinCodeModel
import com.tangem.features.onboarding.v2.visa.impl.child.pincode.ui.OnboardingVisaPinCode
import kotlinx.coroutines.launch
internal class OnboardingVisaPinCodeComponent(
appComponentContext: AppComponentContext,
private val params: Params,
) : ComposableContentComponent, AppComponentContext by appComponentContext {
private val model: OnboardingVisaPinCodeModel = getOrCreateModel()
init {
componentScope.launch {
model.onDone.collect { params.onDone() }
}
}
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
BackHandler(onBack = remember(this) { { params.onBack() } })
DisableScreenshotsDisposableEffect()
OnboardingVisaPinCode(
modifier = modifier,
state = state,
)
}
data class Params(
val onBack: () -> Unit,
val onDone: () -> Unit,
)
}

View file

@ -0,0 +1,52 @@
package com.tangem.features.onboarding.v2.visa.impl.child.pincode.model
import androidx.compose.runtime.Stable
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.model.Model
import com.tangem.features.onboarding.v2.visa.impl.child.pincode.ui.state.OnboardingVisaPinCodeUM
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 OnboardingVisaPinCodeModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
) : Model() {
private val _uiState = MutableStateFlow(getInitialState())
val uiState = _uiState.asStateFlow()
val onDone = MutableSharedFlow<Unit>()
private fun getInitialState(): OnboardingVisaPinCodeUM {
return OnboardingVisaPinCodeUM(
onPinCodeChange = ::onPinCodeChange,
onSubmitClick = ::onSubmitClick,
)
}
private fun onPinCodeChange(pin: String) {
if (pin.all { it.isDigit() }) _uiState.update { it.copy(pinCode = pin) }
}
private fun onSubmitClick() {
if (checkPinCode(_uiState.value.pinCode).not()) return
// TODO
modelScope.launch { onDone.emit(Unit) }
}
private fun checkPinCode(pin: String): Boolean {
return pin.length == PIN_CODE_LENGTH
}
private companion object {
const val PIN_CODE_LENGTH = 4
}
}

View file

@ -0,0 +1,195 @@
package com.tangem.features.onboarding.v2.visa.impl.child.pincode.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.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Color.Companion.Transparent
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.SpacerH16
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.onboarding.v2.visa.impl.child.pincode.ui.state.OnboardingVisaPinCodeUM
import kotlinx.coroutines.delay
@Composable
internal fun OnboardingVisaPinCode(state: OnboardingVisaPinCodeUM, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxSize()
.navigationBarsPadding(),
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(top = 48.dp)
.padding(horizontal = 36.dp)
.weight(1f),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = "Create\nPIN Code",
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
)
SpacerH16()
Text(
text = "Set up a 4-digit code.\nIt will be used for payments.",
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
)
SpacerH(26.dp)
PinCodeSection(state)
}
PrimaryButton(
modifier = Modifier
.imePadding()
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
.fillMaxWidth(),
text = "Submit",
onClick = state.onSubmitClick,
)
}
}
@Composable
private fun PinCodeSection(state: OnboardingVisaPinCodeUM, modifier: Modifier = Modifier) {
val focusRequester = remember { FocusRequester() }
PinCode(
modifier = modifier,
value = state.pinCode,
onValueChange = state.onPinCodeChange,
focusRequester = focusRequester,
)
LaunchedEffect(Unit) {
delay(timeMillis = 300)
focusRequester.requestFocus()
}
}
@Composable
private fun PinCode(
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
numbersCount: Int = 4,
focusRequester: FocusRequester = remember { FocusRequester() },
onDone: ((String) -> Unit)? = null,
) {
val keyboardController = LocalSoftwareKeyboardController.current
Box(
contentAlignment = Alignment.Center,
modifier = modifier,
) {
BasicTextField(
value = value,
onValueChange = { text ->
if (text.length <= numbersCount) {
onValueChange(text)
if (text.length == numbersCount) {
onDone?.invoke(text)
}
}
},
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Number,
imeAction = ImeAction.Done,
),
keyboardActions = KeyboardActions(onDone = {
keyboardController?.hide()
},),
modifier = Modifier
.alpha(alpha = 0.01f)
.focusRequester(focusRequester),
textStyle = TextStyle.Default.copy(color = Transparent),
)
Row(
modifier = Modifier
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() },
onClick = {
keyboardController?.show()
focusRequester.requestFocus()
},
),
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
repeat(numbersCount) { index ->
Box(
modifier = Modifier
.background(
color = Color(color = 0xFFF5F5F5),
shape = TangemTheme.shapes.roundedCornersXMedium,
)
.padding(
vertical = 12.dp,
horizontal = 16.dp,
),
contentAlignment = Alignment.Center,
) {
Text(
modifier = Modifier.widthIn(min = measureTextWidth("0", TangemTheme.typography.h1)),
text = value.getOrNull(index)?.toString() ?: "",
style = TangemTheme.typography.h1,
color = TangemTheme.colors.text.primary1,
)
}
}
}
}
}
@Composable
private fun measureTextWidth(text: String, style: TextStyle): Dp {
val textMeasurer = rememberTextMeasurer()
val widthInPixels = textMeasurer.measure(text, style).size.width
return with(LocalDensity.current) { widthInPixels.toDp() }
}
@Preview(showBackground = true)
@Composable
private fun Preview() {
TangemThemePreview {
OnboardingVisaPinCode(
state = OnboardingVisaPinCodeUM(
pinCode = "1234",
),
)
}
}

View file

@ -0,0 +1,7 @@
package com.tangem.features.onboarding.v2.visa.impl.child.pincode.ui.state
internal data class OnboardingVisaPinCodeUM(
val pinCode: String = "",
val onPinCodeChange: (String) -> Unit = {},
val onSubmitClick: () -> Unit = {},
)

View file

@ -16,16 +16,29 @@ internal class OnboardingVisaWelcomeComponent(
@Composable
override fun Content(modifier: Modifier) {
val state = remember(this) {
OnboardingVisaWelcomeUM(
mode = if (params.isWelcomeBack) {
OnboardingVisaWelcomeUM.Mode.WelcomeBack
} else {
OnboardingVisaWelcomeUM.Mode.Hello
},
userName = "User", // TODO:
onContinueClick = params.onDone,
)
}
BackHandler(onBack = remember(this) { { params.onBack() } })
OnboardingVisaWelcome(
modifier = modifier,
state = OnboardingVisaWelcomeUM(),
state = state,
)
}
data class Params(
val isWelcomeBack: Boolean,
val onBack: () -> Unit,
val startActivation: () -> Unit,
val onDone: () -> Unit,
)
}

View file

@ -1,14 +1,16 @@
package com.tangem.features.onboarding.v2.visa.impl.child.welcome.ui
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.PrimaryButtonIconEnd
import com.tangem.core.ui.components.SpacerH
@ -87,10 +89,12 @@ internal fun OnboardingVisaWelcome(state: OnboardingVisaWelcomeUM, modifier: Mod
}
}
@Suppress("MagicNumber")
@Composable
fun Artwork(modifier: Modifier = Modifier) {
Box(modifier) {
val circleColor = TangemTheme.colors.background.secondary
var cardHeightPx by remember { mutableIntStateOf(0) }
Canvas(
modifier = Modifier
@ -100,17 +104,19 @@ fun Artwork(modifier: Modifier = Modifier) {
) {
drawCircle(
color = circleColor,
radius = size.width / 2,
radius = cardHeightPx / 2 + cardHeightPx / 8f,
center = center,
)
}
AsyncImage(
model = R.drawable.img_card_visa,
Image(
painter = painterResource(R.drawable.img_card_visa),
contentDescription = null,
modifier = Modifier
modifier = modifier
.align(Alignment.Center)
.size(128.dp), // Adjust the size as needed
.onSizeChanged { cardHeightPx = it.height }
.widthIn(max = 512.dp)
.fillMaxWidth(),
)
}
}

View file

@ -1,11 +1,22 @@
package com.tangem.features.onboarding.v2.visa.impl.di
import com.tangem.core.decompose.di.DecomposeComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.onboarding.v2.visa.api.OnboardingVisaComponent
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.approve.model.OnboardingVisaApproveModel
import com.tangem.features.onboarding.v2.visa.impl.child.choosewallet.model.OnboardingVisaChooseWalletModel
import com.tangem.features.onboarding.v2.visa.impl.child.inprogress.model.OnboardingVisaInProgressModel
import com.tangem.features.onboarding.v2.visa.impl.child.otherwallet.model.OnboardingVisaOtherWalletModel
import com.tangem.features.onboarding.v2.visa.impl.child.pincode.model.OnboardingVisaPinCodeModel
import com.tangem.features.onboarding.v2.visa.impl.model.OnboardingVisaModel
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
@ -15,4 +26,44 @@ internal interface ComponentModule {
@Binds
@Singleton
fun bindComponentFactory(factory: DefaultOnboardingVisaComponent.Factory): OnboardingVisaComponent.Factory
}
@Module
@InstallIn(DecomposeComponent::class)
internal interface ModelModule {
@Binds
@IntoMap
@ClassKey(OnboardingVisaAccessCodeModel::class)
fun provideModel(model: OnboardingVisaAccessCodeModel): Model
@Binds
@IntoMap
@ClassKey(OnboardingVisaChooseWalletModel::class)
fun provideModel2(model: OnboardingVisaChooseWalletModel): Model
@Binds
@IntoMap
@ClassKey(OnboardingVisaPinCodeModel::class)
fun provideModel3(model: OnboardingVisaPinCodeModel): Model
@Binds
@IntoMap
@ClassKey(OnboardingVisaApproveModel::class)
fun provideModel4(model: OnboardingVisaApproveModel): Model
@Binds
@IntoMap
@ClassKey(OnboardingVisaOtherWalletModel::class)
fun provideModel5(model: OnboardingVisaOtherWalletModel): Model
@Binds
@IntoMap
@ClassKey(OnboardingVisaInProgressModel::class)
fun provideModel6(model: OnboardingVisaInProgressModel): Model
@Binds
@IntoMap
@ClassKey(OnboardingVisaModel::class)
fun provideModel7(model: OnboardingVisaModel): Model
}

View file

@ -0,0 +1,23 @@
package com.tangem.features.onboarding.v2.visa.impl.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.api.OnboardingVisaComponent
import com.tangem.features.onboarding.v2.visa.impl.route.OnboardingVisaRoute
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import javax.inject.Inject
@Stable
@ComponentScoped
internal class OnboardingVisaModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
) : Model() {
@Suppress("UnusedPrivateMember")
private val params = paramsContainer.require<OnboardingVisaComponent.Params>()
val initialRoute = OnboardingVisaRoute.Welcome(isWelcomeBack = false)
}

View file

@ -0,0 +1,28 @@
package com.tangem.features.onboarding.v2.visa.impl.route
import kotlinx.serialization.Serializable
@Serializable
sealed class OnboardingVisaRoute {
@Serializable
data class Welcome(val isWelcomeBack: Boolean) : OnboardingVisaRoute()
@Serializable
data object AccessCode : OnboardingVisaRoute()
@Serializable
data object ChooseWallet : OnboardingVisaRoute()
@Serializable
data object TangemWalletApproveOption : OnboardingVisaRoute()
@Serializable
data object OtherWalletApproveOption : OnboardingVisaRoute()
@Serializable
data object InProgress : OnboardingVisaRoute()
@Serializable
data object PinCode : OnboardingVisaRoute()
}

View file

@ -2,11 +2,11 @@ package com.tangem.feature.wallet.presentation.wallet.ui.utils
import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import com.google.android.gms.tasks.Task
import com.google.android.play.core.review.ReviewInfo
import com.google.android.play.core.review.ReviewManager
import com.google.android.play.core.review.ReviewManagerFactory
import com.tangem.core.ui.utils.findActivity
import timber.log.Timber
internal object ReviewManagerRequester {
@ -44,13 +44,4 @@ internal object ReviewManagerRequester {
Timber.e(task.exception)
}
}
private fun Context.findActivity(): Activity {
var context = this
while (context is ContextWrapper) {
if (context is Activity) return context
context = context.baseContext
}
error("Permissions should be called in the context of an Activity")
}
}