diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt index 796b99ce27..5551a4af79 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt @@ -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 ) { diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/OutsetBorder.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/OutsetBorder.kt new file mode 100644 index 0000000000..8dfc3ecd52 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/OutsetBorder.kt @@ -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()), + ) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/security/DisableScreenshots.kt b/core/ui/src/main/java/com/tangem/core/ui/security/DisableScreenshots.kt new file mode 100644 index 0000000000..4a421ead77 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/security/DisableScreenshots.kt @@ -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) + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/ContextUtils.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/ContextUtils.kt new file mode 100644 index 0000000000..576cf1f08a --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/ContextUtils.kt @@ -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.") +} \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_loader.xml b/core/ui/src/main/res/drawable/ic_loader.xml new file mode 100644 index 0000000000..5c61f9a912 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_loader.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/core/ui/src/main/res/drawable/ic_tangem_card_grey.xml b/core/ui/src/main/res/drawable/ic_tangem_card_grey.xml new file mode 100644 index 0000000000..fb390ee999 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_tangem_card_grey.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/core/ui/src/main/res/drawable/ic_wallet_filled_24.xml b/core/ui/src/main/res/drawable/ic_wallet_filled_24.xml new file mode 100644 index 0000000000..c2e83ec569 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_wallet_filled_24.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/core/ui/src/main/res/drawable/img_card_visa.webp b/core/ui/src/main/res/drawable/img_card_visa.webp index bd2804ca99..f51b3d8f95 100644 Binary files a/core/ui/src/main/res/drawable/img_card_visa.webp and b/core/ui/src/main/res/drawable/img_card_visa.webp differ diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt index ef564c6194..281ec29e57 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt @@ -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) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingChildFactory.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingChildFactory.kt index a7488de99a..6e614deefd 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingChildFactory.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingChildFactory.kt @@ -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( diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingRoute.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingRoute.kt index 090724dff0..69778da3b4 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingRoute.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingRoute.kt @@ -21,6 +21,7 @@ sealed class OnboardingRoute : Route { data class Visa( val titleProvider: TitleProvider, val scanResponse: ScanResponse, + val onDone: () -> Unit, ) : OnboardingRoute() data class ManageTokens( diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/DefaultOnboardingMultiWalletComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/DefaultOnboardingMultiWalletComponent.kt index 6e7321f878..51bf2171ca 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/DefaultOnboardingMultiWalletComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/DefaultOnboardingMultiWalletComponent.kt @@ -54,7 +54,7 @@ internal class DefaultOnboardingMultiWalletComponent @AssistedInject constructor private val model: OnboardingMultiWalletModel = getOrCreateModel(params) private val stackNavigation = StackNavigation() - 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, diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/MultiWalletSeedPhraseComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/MultiWalletSeedPhraseComponent.kt index 2a9df6122b..6e6722ddb7 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/MultiWalletSeedPhraseComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/MultiWalletSeedPhraseComponent.kt @@ -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("") - } } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/api/OnboardingVisaComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/api/OnboardingVisaComponent.kt index 19cf83fac8..6aa983e077 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/api/OnboardingVisaComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/api/OnboardingVisaComponent.kt @@ -11,6 +11,7 @@ interface OnboardingVisaComponent : ComposableContentComponent, InnerNavigationH data class Params( val scanResponse: ScanResponse, val titleProvider: TitleProvider, + val onDone: () -> Unit, ) interface Factory : ComponentFactory diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/DefaultOnboardingVisaComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/DefaultOnboardingVisaComponent.kt index 67982e4535..4f5b70b7a8 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/DefaultOnboardingVisaComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/DefaultOnboardingVisaComponent.kt @@ -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(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() + } + + private val stackNavigation = StackNavigation() + + private val childStack: Value> = + 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 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 diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/OnboardingVisaAccessCodeComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/OnboardingVisaAccessCodeComponent.kt index 5ffff0e28f..06faafcbd7 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/OnboardingVisaAccessCodeComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/OnboardingVisaAccessCodeComponent.kt @@ -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, val onBack: () -> Unit, + val onDone: () -> Unit, ) } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/model/OnboardingVisaAccessCodeModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/model/OnboardingVisaAccessCodeModel.kt index 24d171ebb9..4281e33a1a 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/model/OnboardingVisaAccessCodeModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/model/OnboardingVisaAccessCodeModel.kt @@ -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() + val onDone = MutableSharedFlow() 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 { diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/ui/OnboardingVisaAccessCode.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/ui/OnboardingVisaAccessCode.kt index f5c8364420..6fefcee506 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/ui/OnboardingVisaAccessCode.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/ui/OnboardingVisaAccessCode.kt @@ -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, diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/ui/state/OnboardingVisaAccessCodeUM.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/ui/state/OnboardingVisaAccessCodeUM.kt index 0cf7770ac0..b707b9067c 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/ui/state/OnboardingVisaAccessCodeUM.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/ui/state/OnboardingVisaAccessCodeUM.kt @@ -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(""), diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/OnboardingVisaApproveComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/OnboardingVisaApproveComponent.kt new file mode 100644 index 0000000000..70a5560fc0 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/OnboardingVisaApproveComponent.kt @@ -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, + ) +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/model/OnboardingVisaApproveModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/model/OnboardingVisaApproveModel.kt new file mode 100644 index 0000000000..11bd07d94b --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/model/OnboardingVisaApproveModel.kt @@ -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() + + private fun getInitialState(): OnboardingVisaApproveUM { + return OnboardingVisaApproveUM( + onApproveClick = ::onApproveClick, + ) + } + + private fun onApproveClick() { + // TODO + modelScope.launch { onDone.emit(Unit) } + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/ui/OnboardingVisaApprove.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/ui/OnboardingVisaApprove.kt new file mode 100644 index 0000000000..bc1b5f0ff9 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/ui/OnboardingVisaApprove.kt @@ -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()) + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/ui/state/OnboardingVisaApproveUM.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/ui/state/OnboardingVisaApproveUM.kt new file mode 100644 index 0000000000..90bb2ef920 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/ui/state/OnboardingVisaApproveUM.kt @@ -0,0 +1,5 @@ +package com.tangem.features.onboarding.v2.visa.impl.child.approve.ui.state + +internal data class OnboardingVisaApproveUM( + val onApproveClick: () -> Unit = {}, +) \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/OnboardingVisaChooseWalletComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/OnboardingVisaChooseWalletComponent.kt new file mode 100644 index 0000000000..f8310b209e --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/OnboardingVisaChooseWalletComponent.kt @@ -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 + } + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/model/OnboardingVisaChooseWalletModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/model/OnboardingVisaChooseWalletModel.kt new file mode 100644 index 0000000000..4f6629cae9 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/model/OnboardingVisaChooseWalletModel.kt @@ -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() + 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) + } + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/ui/OnboardingVisaChooseWallet.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/ui/OnboardingVisaChooseWallet.kt new file mode 100644 index 0000000000..b44e28c2c6 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/ui/OnboardingVisaChooseWallet.kt @@ -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, + 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"), + ), + ), + ) + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/ui/state/OnboardingVisaChooseWalletUM.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/ui/state/OnboardingVisaChooseWalletUM.kt new file mode 100644 index 0000000000..0281417755 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/ui/state/OnboardingVisaChooseWalletUM.kt @@ -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 = persistentListOf(), + val selectedOption: SelectableChainRowUM? = null, + val onOptionSelected: (SelectableChainRowUM) -> Unit = {}, + val onContinueClick: () -> Unit = {}, +) \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/ui/state/SelectableChainRowUM.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/ui/state/SelectableChainRowUM.kt new file mode 100644 index 0000000000..dee93cf95f --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/ui/state/SelectableChainRowUM.kt @@ -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, +) \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/OnboardingVisaInProgressComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/OnboardingVisaInProgressComponent.kt new file mode 100644 index 0000000000..f1fca1c49b --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/OnboardingVisaInProgressComponent.kt @@ -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, + ) +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt new file mode 100644 index 0000000000..9639c4ddc8 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt @@ -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() + + init { + modelScope.launch { + // TODO check state + delay(timeMillis = 2000) + onDone.emit(Unit) + } + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/ui/OnboardingVisaInProgress.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/ui/OnboardingVisaInProgress.kt new file mode 100644 index 0000000000..d30e9a0b79 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/ui/OnboardingVisaInProgress.kt @@ -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() + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/OnboardingVisaOtherWalletComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/OnboardingVisaOtherWalletComponent.kt new file mode 100644 index 0000000000..50ca03977d --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/OnboardingVisaOtherWalletComponent.kt @@ -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, + ) +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/model/OnboardingVisaOtherWalletModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/model/OnboardingVisaOtherWalletModel.kt new file mode 100644 index 0000000000..42254b2c08 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/model/OnboardingVisaOtherWalletModel.kt @@ -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() + + private fun getInitialState(): OnboardingVisaOtherWalletUM { + return OnboardingVisaOtherWalletUM( + onShareClick = ::onShareClicked, + onOpenInBrowserClick = ::onOpenInBrowserClicked, + ) + } + + private fun onShareClicked() { + // TODO + } + + private fun onOpenInBrowserClicked() { + // TODO + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/ui/OnboardingVisaOtherWallet.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/ui/OnboardingVisaOtherWallet.kt new file mode 100644 index 0000000000..4b0a8c2f3f --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/ui/OnboardingVisaOtherWallet.kt @@ -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()) + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/ui/state/OnboardingVisaOtherWalletUM.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/ui/state/OnboardingVisaOtherWalletUM.kt new file mode 100644 index 0000000000..11895f4ca9 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/ui/state/OnboardingVisaOtherWalletUM.kt @@ -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 = {}, +) \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/OnboardingVisaPinCodeComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/OnboardingVisaPinCodeComponent.kt new file mode 100644 index 0000000000..6df8920a6f --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/OnboardingVisaPinCodeComponent.kt @@ -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, + ) +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/model/OnboardingVisaPinCodeModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/model/OnboardingVisaPinCodeModel.kt new file mode 100644 index 0000000000..19f65cfd96 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/model/OnboardingVisaPinCodeModel.kt @@ -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() + + 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 + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/ui/OnboardingVisaPinCode.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/ui/OnboardingVisaPinCode.kt new file mode 100644 index 0000000000..cf47762749 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/ui/OnboardingVisaPinCode.kt @@ -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", + ), + ) + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/ui/state/OnboardingVisaPinCodeUM.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/ui/state/OnboardingVisaPinCodeUM.kt new file mode 100644 index 0000000000..a8d26f44a3 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/ui/state/OnboardingVisaPinCodeUM.kt @@ -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 = {}, +) \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/OnboardingVisaWelcomeComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/OnboardingVisaWelcomeComponent.kt index 0194489e69..3180bdfba5 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/OnboardingVisaWelcomeComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/OnboardingVisaWelcomeComponent.kt @@ -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, ) } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/ui/OnboardingVisaWelcome.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/ui/OnboardingVisaWelcome.kt index 61c2b6e0d8..26516e4075 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/ui/OnboardingVisaWelcome.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/ui/OnboardingVisaWelcome.kt @@ -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(), ) } } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/di/ComponentModule.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/di/ComponentModule.kt index abb9a0cecf..5ee550c847 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/di/ComponentModule.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/di/ComponentModule.kt @@ -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 } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/model/OnboardingVisaModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/model/OnboardingVisaModel.kt new file mode 100644 index 0000000000..f3d92c1ea3 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/model/OnboardingVisaModel.kt @@ -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() + + val initialRoute = OnboardingVisaRoute.Welcome(isWelcomeBack = false) +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/route/OnboardingVisaRoute.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/route/OnboardingVisaRoute.kt new file mode 100644 index 0000000000..8eba780fd5 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/route/OnboardingVisaRoute.kt @@ -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() +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/ReviewManagerRequester.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/ReviewManagerRequester.kt index 1826afb74d..a04cb17d49 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/ReviewManagerRequester.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/ReviewManagerRequester.kt @@ -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") - } } \ No newline at end of file