Updated on 2026-08-14

This commit is contained in:
Tangem 2024-11-22 12:04:05 +03:00
parent 8a51041f7f
commit 52eddd3ac6
15 changed files with 639 additions and 33 deletions

View file

@ -1,16 +1,21 @@
package com.tangem.core.ui.components.bottomsheets
import androidx.activity.compose.BackHandler
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
import androidx.compose.foundation.background
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.*
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.SheetState
import androidx.compose.material3.*
import androidx.compose.material3.SheetValue.Expanded
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.input.key.*
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.extensions.TextReference
@ -18,6 +23,7 @@ import com.tangem.core.ui.res.LocalBottomSheetAlwaysVisible
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.WindowInsetsZero
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
/**
@ -31,6 +37,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> TangemBottomSheet(
containerColor: Color = TangemTheme.colors.background.primary,
addBottomInsets: Boolean = true,
skipPartiallyExpanded: Boolean = true,
noinline onBack: (() -> Unit)? = null,
crossinline content: @Composable ColumnScope.(T) -> Unit,
) {
TangemBottomSheet(
@ -39,6 +46,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> TangemBottomSheet(
addBottomInsets = addBottomInsets,
title = { TangemBottomSheetTitle(title = titleText, endButton = titleAction) },
skipPartiallyExpanded = skipPartiallyExpanded,
onBack = onBack,
content = content,
)
}
@ -52,6 +60,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> TangemBottomSheet(
containerColor: Color = TangemTheme.colors.background.primary,
addBottomInsets: Boolean = true,
skipPartiallyExpanded: Boolean = true,
noinline onBack: (() -> Unit)? = null,
crossinline title: @Composable BoxScope.(T) -> Unit = {},
crossinline content: @Composable ColumnScope.(T) -> Unit,
) {
@ -73,6 +82,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> TangemBottomSheet(
addBottomInsets = addBottomInsets,
title = title,
content = content,
onBack = onBack,
skipPartiallyExpanded = skipPartiallyExpanded,
)
}
@ -85,6 +95,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> DefaultBottomSheet(
containerColor: Color,
addBottomInsets: Boolean,
skipPartiallyExpanded: Boolean = true,
noinline onBack: (() -> Unit)? = null,
crossinline title: @Composable (BoxScope.(T) -> Unit),
crossinline content: @Composable (ColumnScope.(T) -> Unit),
) {
@ -98,6 +109,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> DefaultBottomSheet(
containerColor = containerColor,
addBottomInsets = addBottomInsets,
title = title,
onBack = onBack,
content = content,
)
}
@ -129,6 +141,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> PreviewBottomSheet(
initialValue = Expanded,
density = LocalDensity.current,
),
onBack = null,
containerColor = containerColor,
addBottomInsets = addBottomInsets,
title = title,
@ -144,6 +157,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicBottomSheet(
sheetState: SheetState,
containerColor: Color,
addBottomInsets: Boolean,
noinline onBack: (() -> Unit)? = null,
crossinline title: @Composable (BoxScope.(T) -> Unit),
crossinline content: @Composable (ColumnScope.(T) -> Unit),
modifier: Modifier = Modifier,
@ -152,15 +166,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicBottomSheet(
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
ModalBottomSheet(
modifier = modifier.statusBarsPadding(),
onDismissRequest = config.onDismissRequest,
sheetState = sheetState,
containerColor = containerColor,
shape = TangemTheme.shapes.bottomSheetLarge,
contentWindowInsets = { WindowInsetsZero },
dragHandle = { TangemBottomSheetDraggableHeader(color = containerColor) },
) {
val bsContent: @Composable ColumnScope.() -> Unit = {
Column(
modifier = Modifier.let {
if (addBottomInsets) {
@ -179,6 +185,90 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicBottomSheet(
content(model)
}
}
if (onBack != null) {
ModalBottomSheetWithBackHandling(
modifier = modifier.statusBarsPadding(),
onDismissRequest = config.onDismissRequest,
sheetState = sheetState,
containerColor = containerColor,
shape = TangemTheme.shapes.bottomSheetLarge,
contentWindowInsets = { WindowInsetsZero },
dragHandle = { TangemBottomSheetDraggableHeader(color = containerColor) },
onBack = onBack,
content = bsContent,
)
} else {
ModalBottomSheet(
modifier = modifier.statusBarsPadding(),
onDismissRequest = config.onDismissRequest,
sheetState = sheetState,
containerColor = containerColor,
shape = TangemTheme.shapes.bottomSheetLarge,
contentWindowInsets = { WindowInsetsZero },
dragHandle = { TangemBottomSheetDraggableHeader(color = containerColor) },
content = bsContent,
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ModalBottomSheetWithBackHandling(
onDismissRequest: () -> Unit,
onBack: (() -> Unit),
modifier: Modifier = Modifier,
sheetState: SheetState = rememberModalBottomSheetState(),
sheetMaxWidth: Dp = BottomSheetDefaults.SheetMaxWidth,
shape: Shape = BottomSheetDefaults.ExpandedShape,
containerColor: Color = BottomSheetDefaults.ContainerColor,
contentColor: Color = contentColorFor(containerColor),
tonalElevation: Dp = 0.dp,
scrimColor: Color = BottomSheetDefaults.ScrimColor,
dragHandle: @Composable (() -> Unit)? = { BottomSheetDefaults.DragHandle() },
contentWindowInsets: @Composable () -> WindowInsets = { BottomSheetDefaults.windowInsets },
content: @Composable ColumnScope.() -> Unit,
) {
BackHandler(enabled = sheetState.targetValue != SheetValue.Hidden) {
onBack()
}
val requester = remember { FocusRequester() }
val backPressedDispatcherOwner = LocalOnBackPressedDispatcherOwner.current
ModalBottomSheet(
onDismissRequest = onDismissRequest,
modifier = modifier
.focusRequester(requester)
.focusable()
.onPreviewKeyEvent {
if (it.key == Key.Back && it.type == KeyEventType.KeyUp && !it.nativeKeyEvent.isCanceled) {
backPressedDispatcherOwner?.onBackPressedDispatcher?.onBackPressed()
return@onPreviewKeyEvent true
}
return@onPreviewKeyEvent false
},
sheetState = sheetState,
sheetMaxWidth = sheetMaxWidth,
shape = shape,
containerColor = containerColor,
contentColor = contentColor,
tonalElevation = tonalElevation,
scrimColor = scrimColor,
dragHandle = dragHandle,
contentWindowInsets = contentWindowInsets,
properties = ModalBottomSheetDefaults.properties(
// Set false otherwise the onPreviewKeyEvent doesn't work at all.
// The functionality of shouldDismissOnBackPress is achieved by the BackHandler.
shouldDismissOnBackPress = false,
),
content = content,
)
LaunchedEffect(Unit) {
delay(timeMillis = 200)
requester.requestFocus()
}
}
@OptIn(ExperimentalMaterial3Api::class)

View file

@ -7,8 +7,6 @@ import androidx.compose.runtime.*
@Immutable
object TangemAnimations {
val transitionSpecs = TransitionSpecs
@Composable
@NonRestartableComposable
fun horizontalIndicatorAsState(targetFraction: Float): State<Float> {
@ -19,8 +17,18 @@ object TangemAnimations {
)
}
@Immutable
object TransitionSpecs {
// TODO add more transition specs
object AnimatedContent {
fun <S> slide(
forwardWhen: (initial: S, target: S) -> Boolean,
): AnimatedContentTransitionScope<S>.() -> ContentTransform = {
val direction = if (forwardWhen(initialState, targetState)) {
AnimatedContentTransitionScope.SlideDirection.End
} else {
AnimatedContentTransitionScope.SlideDirection.Start
}
slideIntoContainer(towards = direction, animationSpec = tween())
.togetherWith(slideOutOfContainer(towards = direction, animationSpec = tween()))
}
}
}

View file

@ -0,0 +1,24 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="119dp"
android:height="119dp"
android:viewportWidth="119"
android:viewportHeight="119">
<path
android:pathData="M111.664,30.74H7.336C3.291,30.74 0,34.03 0,38.075V60.431C0,64.476 3.291,67.767 7.336,67.767H72.541C73.506,67.767 74.288,66.985 74.288,66.021C74.288,65.056 73.506,64.274 72.541,64.274H7.336C5.217,64.274 3.493,62.55 3.493,60.431V38.075C3.493,35.957 5.217,34.233 7.336,34.233H111.664C113.783,34.233 115.507,35.957 115.507,38.075V54.842C115.507,55.807 116.289,56.589 117.253,56.589C118.218,56.589 119,55.807 119,54.842V38.075C119,34.03 115.709,30.74 111.664,30.74Z"
android:fillColor="#060606"/>
<path
android:pathData="M111.548,58.972V56.706C111.548,48.552 104.914,41.918 96.76,41.918C88.606,41.918 81.973,48.552 81.973,56.706V58.972C79.809,59.703 78.247,61.751 78.247,64.158V82.788C78.247,85.805 80.702,88.26 83.719,88.26H109.801C112.819,88.26 115.274,85.805 115.274,82.788V64.158C115.274,61.751 113.711,59.703 111.548,58.972ZM96.76,45.411C102.988,45.411 108.055,50.478 108.055,56.706V58.685H85.466V56.706C85.466,50.478 90.533,45.411 96.76,45.411ZM111.781,82.788C111.781,83.879 110.893,84.767 109.801,84.767H83.719C82.628,84.767 81.74,83.879 81.74,82.788V64.158C81.74,63.066 82.628,62.178 83.719,62.178H109.801C110.893,62.178 111.781,63.066 111.781,64.158V82.788Z"
android:fillColor="#060606"/>
<path
android:pathData="M28.343,44.654C27.861,43.819 26.793,43.533 25.957,44.015L22.123,46.228V41.801C22.123,40.837 21.341,40.055 20.377,40.055C19.412,40.055 18.63,40.837 18.63,41.801V46.228L14.796,44.015C13.961,43.532 12.893,43.819 12.411,44.654C11.928,45.489 12.215,46.558 13.05,47.04L16.884,49.253L13.05,51.467C12.215,51.949 11.928,53.017 12.411,53.853C12.734,54.413 13.321,54.726 13.925,54.726C14.221,54.726 14.521,54.651 14.796,54.492L18.63,52.278V56.706C18.63,57.67 19.412,58.452 20.377,58.452C21.341,58.452 22.123,57.67 22.123,56.706V52.278L25.957,54.492C26.232,54.651 26.533,54.726 26.829,54.726C27.433,54.726 28.02,54.413 28.343,53.853C28.825,53.017 28.539,51.949 27.704,51.467L23.87,49.253L27.704,47.04C28.539,46.558 28.825,45.489 28.343,44.654Z"
android:fillColor="#060606"/>
<path
android:pathData="M50.699,44.654C50.217,43.819 49.149,43.533 48.313,44.015L44.48,46.228V41.801C44.48,40.837 43.698,40.055 42.733,40.055C41.768,40.055 40.986,40.837 40.986,41.801V46.228L37.153,44.015C36.317,43.532 35.249,43.819 34.767,44.654C34.285,45.489 34.571,46.558 35.406,47.04L39.24,49.253L35.406,51.467C34.571,51.949 34.285,53.017 34.767,53.853C35.09,54.413 35.677,54.726 36.281,54.726C36.577,54.726 36.877,54.651 37.153,54.492L40.986,52.278V56.706C40.986,57.67 41.768,58.452 42.733,58.452C43.698,58.452 44.48,57.67 44.48,56.706V52.278L48.313,54.492C48.588,54.651 48.889,54.726 49.185,54.726C49.789,54.726 50.376,54.413 50.699,53.853C51.181,53.017 50.895,51.949 50.06,51.467L46.226,49.253L50.06,47.04C50.895,46.558 51.181,45.489 50.699,44.654Z"
android:fillColor="#060606"/>
<path
android:pathData="M73.055,44.654C72.573,43.819 71.505,43.533 70.67,44.015L66.836,46.228V41.801C66.836,40.837 66.054,40.055 65.089,40.055C64.125,40.055 63.343,40.837 63.343,41.801V46.228L59.509,44.015C58.673,43.532 57.605,43.819 57.123,44.654C56.641,45.489 56.927,46.558 57.762,47.04L61.596,49.253L57.762,51.467C56.927,51.949 56.641,53.017 57.123,53.853C57.446,54.413 58.034,54.726 58.637,54.726C58.933,54.726 59.234,54.651 59.509,54.492L63.343,52.278V56.706C63.343,57.67 64.125,58.452 65.089,58.452C66.054,58.452 66.836,57.67 66.836,56.706V52.278L70.67,54.492C70.945,54.651 71.245,54.726 71.541,54.726C72.145,54.726 72.732,54.413 73.055,53.853C73.538,53.017 73.252,51.949 72.416,51.467L68.582,49.253L72.416,47.04C73.252,46.558 73.538,45.489 73.055,44.654Z"
android:fillColor="#060606"/>
<path
android:pathData="M96.76,66.137C93.743,66.137 91.288,68.592 91.288,71.61C91.288,74.017 92.85,76.064 95.014,76.795V79.062C95.014,80.026 95.796,80.808 96.76,80.808C97.725,80.808 98.507,80.026 98.507,79.062V76.795C100.67,76.064 102.233,74.017 102.233,71.61C102.233,68.592 99.778,66.137 96.76,66.137ZM96.76,73.589C95.669,73.589 94.781,72.701 94.781,71.61C94.781,70.518 95.669,69.63 96.76,69.63C97.852,69.63 98.74,70.518 98.74,71.61C98.74,72.701 97.852,73.589 96.76,73.589Z"
android:fillColor="#060606"/>
</vector>

View file

@ -0,0 +1,22 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="25dp"
android:height="25dp"
android:viewportWidth="25"
android:viewportHeight="25">
<path
android:pathData="M3,11H11C12.6569,11 14,12.3431 14,14V22C14,23.6569 12.6569,25 11,25H3C1.3432,25 0,23.6569 0,22V14C0,12.3431 1.3432,11 3,11Z"
android:fillColor="#1ACE80"
android:fillType="evenOdd"/>
<path
android:pathData="M18.7143,11H23.2857C24.2325,11 25,11.7675 25,12.7143V17.2857C25,18.2325 24.2325,19 23.2857,19H18.7143C17.7675,19 17,18.2325 17,17.2857V12.7143C17,11.7675 17.7675,11 18.7143,11Z"
android:strokeAlpha="0.15"
android:fillColor="#1ACE80"
android:fillType="evenOdd"
android:fillAlpha="0.15"/>
<path
android:pathData="M7.7143,0H19.2857C20.2325,0 21,0.7675 21,1.7143V6.2857C21,7.2325 20.2325,8 19.2857,8H7.7143C6.7675,8 6,7.2325 6,6.2857V1.7143C6,0.7675 6.7675,0 7.7143,0Z"
android:strokeAlpha="0.5"
android:fillColor="#1ACE80"
android:fillType="evenOdd"
android:fillAlpha="0.5"/>
</vector>

View file

@ -0,0 +1,22 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="25dp"
android:height="25dp"
android:viewportWidth="25"
android:viewportHeight="25">
<path
android:pathData="M18,25C21.866,25 25,21.866 25,18C25,14.134 21.866,11 18,11C14.134,11 11,14.134 11,18C11,21.866 14.134,25 18,25Z"
android:fillColor="#1ACE80"
android:fillType="evenOdd"/>
<path
android:pathData="M9,10C11.7614,10 14,7.7614 14,5C14,2.2386 11.7614,0 9,0C6.2386,0 4,2.2386 4,5C4,7.7614 6.2386,10 9,10Z"
android:strokeAlpha="0.5"
android:fillColor="#1ACE80"
android:fillType="evenOdd"
android:fillAlpha="0.5"/>
<path
android:pathData="M4,21C6.2091,21 8,19.2091 8,17C8,14.7909 6.2091,13 4,13C1.7909,13 0,14.7909 0,17C0,19.2091 1.7909,21 4,21Z"
android:strokeAlpha="0.15"
android:fillColor="#1ACE80"
android:fillType="evenOdd"
android:fillAlpha="0.15"/>
</vector>

View file

@ -0,0 +1,22 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="26dp"
android:height="22dp"
android:viewportWidth="26"
android:viewportHeight="22">
<path
android:pathData="M19.3989,4.1971L13.3558,4.1971C12.3137,4.1512 11.3955,4.9319 11.3048,5.9408C11.2723,6.3027 11.3515,6.6613 11.5324,6.9712L14.7623,12.5027C15.2667,13.3667 16.4185,13.6429 17.3349,13.1197C17.6423,12.9442 17.8966,12.6917 18.0696,12.3902L21.041,6.9712C21.5566,6.0725 21.1048,4.9181 20.1946,4.4571C19.9583,4.3374 19.6695,4.209 19.3989,4.1971Z"
android:strokeAlpha="0.5"
android:fillColor="#1ACE80"
android:fillType="evenOdd"
android:fillAlpha="0.5"/>
<path
android:pathData="M20.1304,14.9339L17.4396,19.051C16.9906,19.738 17.1702,20.6677 17.8409,21.1276C18.0815,21.2926 18.3644,21.3806 18.6539,21.3806H23.8216C24.6287,21.3806 25.2829,20.7105 25.2829,19.8837C25.2829,19.6064 25.2077,19.3345 25.0657,19.0984L22.5889,14.9813C22.1655,14.2775 21.2652,14.0586 20.5781,14.4923C20.3997,14.6049 20.247,14.7555 20.1304,14.9339Z"
android:strokeAlpha="0.15"
android:fillColor="#1ACE80"
android:fillType="evenOdd"
android:fillAlpha="0.15"/>
<path
android:pathData="M5.1416,10.4031L0.4325,17.4279C-0.3532,18.6 -0.0387,20.1864 1.1349,20.9711C1.5559,21.2526 2.051,21.4029 2.5576,21.4029H11.6011C13.0135,21.4029 14.1585,20.2594 14.1585,18.8488C14.1585,18.3756 14.0269,17.9117 13.7783,17.5089L9.4439,10.4841C8.7029,9.2832 7.1275,8.9096 5.925,9.6496C5.6128,9.8417 5.3456,10.0987 5.1416,10.4031Z"
android:fillColor="#1ACE80"
android:fillType="evenOdd"/>
</vector>

View file

@ -10,6 +10,10 @@ import com.arkivanov.decompose.extensions.compose.jetpack.stack.animation.fade
import com.arkivanov.decompose.extensions.compose.jetpack.stack.animation.slide
import com.arkivanov.decompose.extensions.compose.jetpack.stack.animation.stackAnimation
import com.arkivanov.decompose.extensions.compose.jetpack.subscribeAsState
import com.arkivanov.decompose.router.slot.SlotNavigation
import com.arkivanov.decompose.router.slot.childSlot
import com.arkivanov.decompose.router.slot.dismiss
import com.arkivanov.decompose.router.slot.navigate
import com.arkivanov.decompose.router.stack.*
import com.arkivanov.decompose.value.Value
import com.arkivanov.essenty.instancekeeper.getOrCreateSimple
@ -21,6 +25,7 @@ import com.tangem.core.decompose.navigation.inner.InnerNavigationState
import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent
import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildComponent
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
import com.tangem.features.onboarding.v2.multiwallet.impl.child.chooseoption.Wallet1ChooseOptionComponent
import com.tangem.features.onboarding.v2.multiwallet.impl.child.createwallet.MultiWalletCreateWalletComponent
@ -56,6 +61,11 @@ internal class DefaultOnboardingMultiWalletComponent @AssistedInject constructor
)
}
private val childParams = MultiWalletChildParams(
multiWalletState = model.state,
parentParams = params,
)
override val innerNavigation: InnerNavigation = object : InnerNavigation {
override val state = MutableStateFlow(
MultiWalletInnerNavigationState(1, 5),
@ -82,6 +92,24 @@ internal class DefaultOnboardingMultiWalletComponent @AssistedInject constructor
},
)
private val bottomSheetNavigation = SlotNavigation<Unit>()
private val accessCodeBottomSheetSlot = childSlot(
source = bottomSheetNavigation,
serializer = null,
handleBackButton = false,
childFactory = { configuration, componentContext ->
MultiWalletAccessCodeComponent(
context = childByContext(componentContext),
params = childParams,
onDismiss = { bottomSheetNavigation.dismiss() },
)
},
)
init {
bottomSheetNavigation.navigate {}
}
private fun createChild(
step: OnboardingMultiWalletState.Step,
childContext: AppComponentContext,
@ -121,7 +149,7 @@ internal class DefaultOnboardingMultiWalletComponent @AssistedInject constructor
}
}
fun handleNavigationEvent(nextStep: OnboardingMultiWalletState.Step) {
private fun handleNavigationEvent(nextStep: OnboardingMultiWalletState.Step) {
when (nextStep) {
ChooseBackupOption -> {
artworksState.value = WalletArtworksState.Fan
@ -141,7 +169,7 @@ internal class DefaultOnboardingMultiWalletComponent @AssistedInject constructor
stackNavigation.push(nextStep)
}
fun handleBackupComponentEvent(event: MultiWalletBackupComponent.Event) {
private fun handleBackupComponentEvent(event: MultiWalletBackupComponent.Event) {
when (event) {
MultiWalletBackupComponent.Event.Done -> {
// TODO navigate to finalize
@ -184,6 +212,9 @@ internal class DefaultOnboardingMultiWalletComponent @AssistedInject constructor
}
},
)
val bottomSheetState by accessCodeBottomSheetSlot.subscribeAsState()
bottomSheetState.child?.instance?.BottomSheet()
}
@AssistedFactory

View file

@ -0,0 +1,58 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.child.accesscode
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams
import com.tangem.features.onboarding.v2.multiwallet.impl.child.accesscode.model.MultiWalletAccessCodeModel
import com.tangem.features.onboarding.v2.multiwallet.impl.child.accesscode.ui.MultiWalletAccessCodeBS
import kotlinx.coroutines.launch
@Suppress("UnusedPrivateMember")
class MultiWalletAccessCodeComponent(
context: AppComponentContext,
private val params: MultiWalletChildParams,
private val onDismiss: () -> Unit,
) : AppComponentContext by context, ComposableBottomSheetComponent {
private val model = getOrCreateModel<MultiWalletAccessCodeModel>()
init {
componentScope.launch {
model.onDismiss.collect {
onDismiss()
}
}
}
override fun dismiss() {
onDismiss()
}
@Composable
override fun BottomSheet() {
val state by model.uiState.collectAsStateWithLifecycle()
val bsConfig = remember(this) {
TangemBottomSheetConfig(
isShow = true,
onDismissRequest = onDismiss,
content = TangemBottomSheetConfigContent.Empty,
)
}
MultiWalletAccessCodeBS(
config = bsConfig,
state = state,
onBack = { model.onBack() },
)
}
@JvmInline
value class AccessCode(val value: String)
}

View file

@ -0,0 +1,60 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.child.accesscode.model
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.model.Model
import com.tangem.features.onboarding.v2.multiwallet.impl.child.accesscode.ui.state.MultiWalletAccessCodeUM
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
@ComponentScoped
internal class MultiWalletAccessCodeModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
) : Model() {
private val _uiState = MutableStateFlow(getInitialState())
val uiState = _uiState.asStateFlow()
val onDismiss = MutableSharedFlow<Unit>()
fun onBack() {
when (_uiState.value.step) {
MultiWalletAccessCodeUM.Step.Intro -> {
modelScope.launch { onDismiss.emit(Unit) }
}
MultiWalletAccessCodeUM.Step.AccessCode -> {
_uiState.update {
it.copy(step = MultiWalletAccessCodeUM.Step.Intro)
}
}
MultiWalletAccessCodeUM.Step.ConfirmAccessCode -> {
_uiState.update {
it.copy(step = MultiWalletAccessCodeUM.Step.AccessCode)
}
}
}
}
private fun getInitialState() = MultiWalletAccessCodeUM(
onContinue = ::onContinue,
)
private fun onContinue() {
when (_uiState.value.step) {
MultiWalletAccessCodeUM.Step.Intro -> {
_uiState.update { it.copy(step = MultiWalletAccessCodeUM.Step.AccessCode) }
}
MultiWalletAccessCodeUM.Step.AccessCode -> {
_uiState.update { it.copy(step = MultiWalletAccessCodeUM.Step.ConfirmAccessCode) }
}
MultiWalletAccessCodeUM.Step.ConfirmAccessCode -> {
// TODO check
modelScope.launch { onDismiss.emit(Unit) }
}
}
}
}

View file

@ -0,0 +1,75 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.child.accesscode.ui
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.res.TangemAnimations
import com.tangem.features.onboarding.v2.impl.R
import com.tangem.features.onboarding.v2.multiwallet.impl.child.accesscode.ui.state.MultiWalletAccessCodeUM
import com.tangem.features.onboarding.v2.multiwallet.impl.child.accesscode.ui.state.MultiWalletAccessCodeUM.Step.*
@Composable
internal fun MultiWalletAccessCodeBS(
config: TangemBottomSheetConfig,
state: MultiWalletAccessCodeUM,
onBack: () -> Unit,
) {
TangemBottomSheet<TangemBottomSheetConfigContent.Empty>(
config = config,
onBack = onBack,
) { _ ->
Content(state)
}
}
@Composable
private fun Content(state: MultiWalletAccessCodeUM, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxSize(),
) {
AnimatedContent(
modifier = Modifier.weight(1f),
targetState = state.step,
transitionSpec = TangemAnimations.AnimatedContent
.slide { initial, target -> initial.ordinal > target.ordinal },
label = "AnimatedContent",
) { step ->
when (step) {
Intro -> MultiWalletAccessCodeIntro(
modifier = Modifier.padding(top = 56.dp),
)
AccessCode -> MultiWalletAccessCodeEnter(
modifier = Modifier.padding(top = 16.dp),
state = state,
reEnterAccessCodeState = false,
)
ConfirmAccessCode -> MultiWalletAccessCodeEnter(
modifier = Modifier.padding(top = 16.dp),
state = state,
reEnterAccessCodeState = true,
)
}
}
PrimaryButton(
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
.imePadding(),
text = if (state.step == ConfirmAccessCode) {
stringResource(R.string.common_confirm)
} else {
stringResource(R.string.common_continue)
},
onClick = state.onContinue,
)
}
}

View file

@ -0,0 +1,89 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.child.accesscode.ui
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.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.OutlineTextField
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.onboarding.v2.impl.R
import com.tangem.features.onboarding.v2.multiwallet.impl.child.accesscode.ui.state.MultiWalletAccessCodeUM
@Composable
internal fun MultiWalletAccessCodeEnter(
state: MultiWalletAccessCodeUM,
reEnterAccessCodeState: Boolean,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.padding(horizontal = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(32.dp),
) {
Text(
text = if (reEnterAccessCodeState) {
stringResource(R.string.onboarding_access_code_repeat_code_title)
} else {
stringResource(R.string.onboarding_access_code_intro_title)
},
style = TangemTheme.typography.subtitle1,
)
Text(
text = if (reEnterAccessCodeState) {
stringResource(R.string.onboarding_access_code_repeat_code_hint)
} else {
stringResource(R.string.onboarding_access_code_hint)
},
style = TangemTheme.typography.body1,
)
OutlineTextField(
modifier = modifier.fillMaxWidth(),
value = if (reEnterAccessCodeState) {
state.accessCodeSecond
} else {
state.accessCodeFirst
},
onValueChange = if (reEnterAccessCodeState) {
state.onAccessCodeSecondChange
} else {
state.onAccessCodeFirstChange
},
label = stringResource(id = R.string.onboarding_wallet_info_title_third),
isError = state.codesNotMatchError,
caption = if (state.codesNotMatchError && reEnterAccessCodeState) {
stringResource(R.string.onboarding_access_codes_doesnt_match)
} else {
null
},
)
}
}
@Preview(showBackground = true)
@Composable
private fun Preview() {
TangemThemePreview {
MultiWalletAccessCodeEnter(
reEnterAccessCodeState = false,
state = MultiWalletAccessCodeUM(),
)
}
}
@Preview(showBackground = true)
@Composable
private fun Preview2() {
TangemThemePreview {
MultiWalletAccessCodeEnter(
reEnterAccessCodeState = true,
state = MultiWalletAccessCodeUM(),
)
}
}

View file

@ -0,0 +1,91 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.child.accesscode.ui
import androidx.annotation.DrawableRes
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.stringResource
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.SpacerH
import com.tangem.core.ui.components.SpacerW
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.onboarding.v2.impl.R
@Composable
internal fun MultiWalletAccessCodeIntro(modifier: Modifier = Modifier) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
Text(
text = stringResource(R.string.onboarding_access_code_intro_title),
style = TangemTheme.typography.h2,
)
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_access_code),
tint = TangemTheme.colors.icon.informative,
contentDescription = null,
)
Column(
modifier = Modifier
.padding(horizontal = 46.dp)
.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(24.dp),
) {
DescriptionItem(
title = stringResource(R.string.onboarding_access_code_feature_1_title),
body = stringResource(R.string.onboarding_access_code_feature_1_description),
iconRes = R.drawable.ic_feature_1,
)
DescriptionItem(
title = stringResource(R.string.onboarding_access_code_feature_2_title),
body = stringResource(R.string.onboarding_access_code_feature_2_description),
iconRes = R.drawable.ic_feature_2,
)
DescriptionItem(
title = stringResource(R.string.onboarding_access_code_feature_3_title),
body = stringResource(R.string.onboarding_access_code_feature_3_description),
iconRes = R.drawable.ic_feature_3,
)
}
}
}
@Composable
private fun DescriptionItem(title: String, body: String, @DrawableRes iconRes: Int, modifier: Modifier = Modifier) {
Row(modifier) {
Icon(
imageVector = ImageVector.vectorResource(iconRes),
tint = TangemTheme.colors.icon.primary1,
contentDescription = null,
)
SpacerW(20.dp)
Column {
Text(
text = title,
style = TangemTheme.typography.subtitle1,
)
SpacerH(3.dp)
Text(
text = body,
style = TangemTheme.typography.body2,
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun Preview() {
TangemThemePreview {
MultiWalletAccessCodeIntro()
}
}

View file

@ -0,0 +1,19 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.child.accesscode.ui.state
import androidx.compose.ui.text.input.TextFieldValue
internal data class MultiWalletAccessCodeUM(
val step: Step = Step.Intro,
val accessCodeFirst: TextFieldValue = TextFieldValue(""),
val accessCodeSecond: TextFieldValue = TextFieldValue(""),
val codesNotMatchError: Boolean = false,
val onAccessCodeFirstChange: (TextFieldValue) -> Unit = {},
val onAccessCodeSecondChange: (TextFieldValue) -> Unit = {},
val onContinue: () -> Unit = {},
) {
enum class Step {
Intro,
AccessCode,
ConfirmAccessCode,
}
}

View file

@ -1,12 +1,10 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedContentTransitionScope
import androidx.compose.animation.core.tween
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.ui.res.TangemAnimations
import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui.state.MultiWalletSeedPhraseUM
@Composable
@ -14,16 +12,7 @@ internal fun MultiWalletSeedPhrase(state: MultiWalletSeedPhraseUM, modifier: Mod
AnimatedContent(
modifier = modifier.navigationBarsPadding(),
targetState = state,
transitionSpec = {
val direction = if (initialState.order < targetState.order) {
AnimatedContentTransitionScope.SlideDirection.Start
} else {
AnimatedContentTransitionScope.SlideDirection.End
}
slideIntoContainer(towards = direction, animationSpec = tween())
.togetherWith(slideOutOfContainer(towards = direction, animationSpec = tween()))
},
transitionSpec = TangemAnimations.AnimatedContent.slide { from, to -> to.order > from.order },
contentKey = { st -> st::class.java },
label = "animatedContent",
) { st ->

View file

@ -4,6 +4,7 @@ import com.tangem.core.decompose.di.DecomposeComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent
import com.tangem.features.onboarding.v2.multiwallet.impl.DefaultOnboardingMultiWalletComponent
import com.tangem.features.onboarding.v2.multiwallet.impl.child.accesscode.model.MultiWalletAccessCodeModel
import com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.model.MultiWalletBackupModel
import com.tangem.features.onboarding.v2.multiwallet.impl.child.createwallet.model.MultiWalletCreateWalletModel
import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model.MultiWalletSeedPhraseModel
@ -48,4 +49,9 @@ internal interface ModelModule {
@IntoMap
@ClassKey(MultiWalletSeedPhraseModel::class)
fun provideModel4(model: MultiWalletSeedPhraseModel): Model
@Binds
@IntoMap
@ClassKey(MultiWalletAccessCodeModel::class)
fun provideModel5(model: MultiWalletAccessCodeModel): Model
}