Updated on 2026-08-14

This commit is contained in:
Tangem 2025-10-10 13:51:10 +05:00
parent 499351d5a9
commit 2bc8c29f6d
31 changed files with 1086 additions and 7 deletions

View file

@ -273,6 +273,8 @@ dependencies {
implementation(projects.features.welcome.impl)
implementation(projects.features.createWalletSelection.api)
implementation(projects.features.createWalletSelection.impl)
implementation(projects.features.createWalletStart.api)
implementation(projects.features.createWalletStart.impl)
implementation(projects.features.home.api)
implementation(projects.features.home.impl)
implementation(projects.features.account.api)

View file

@ -13,6 +13,7 @@ import com.tangem.features.account.AccountCreateEditComponent
import com.tangem.features.account.AccountDetailsComponent
import com.tangem.features.account.ArchivedAccountListComponent
import com.tangem.features.createwalletselection.CreateWalletSelectionComponent
import com.tangem.features.createwalletstart.CreateWalletStartComponent
import com.tangem.features.details.component.DetailsComponent
import com.tangem.features.disclaimer.api.components.DisclaimerComponent
import com.tangem.features.home.api.HomeComponent
@ -99,6 +100,7 @@ internal class ChildFactory @Inject constructor(
private val usedeskComponentFactory: UsedeskComponent.Factory,
private val chooseManagedTokensComponentFactory: ChooseManagedTokensComponent.Factory,
private val createWalletSelectionComponentFactory: CreateWalletSelectionComponent.Factory,
private val createWalletStartComponentFactory: CreateWalletStartComponent.Factory,
private val createMobileWalletComponentFactory: CreateMobileWalletComponent.Factory,
private val upgradeWalletComponentFactory: UpgradeWalletComponent.Factory,
private val addExistingWalletComponentFactory: AddExistingWalletComponent.Factory,
@ -476,6 +478,19 @@ internal class ChildFactory @Inject constructor(
componentFactory = chooseManagedTokensComponentFactory,
)
}
is AppRoute.CreateWalletStart -> {
val mode = when (route.mode) {
AppRoute.CreateWalletStart.Mode.ColdWallet -> CreateWalletStartComponent.Mode.ColdWallet
AppRoute.CreateWalletStart.Mode.HotWallet -> CreateWalletStartComponent.Mode.HotWallet
}
createComponentChild(
context = context,
params = CreateWalletStartComponent.Params(
mode = mode,
),
componentFactory = createWalletStartComponentFactory,
)
}
is AppRoute.CreateWalletSelection -> {
createComponentChild(
context = context,

View file

@ -311,6 +311,16 @@ sealed class AppRoute(val path: String) : Route {
@Serializable
object CreateWalletSelection : AppRoute(path = "/create_wallet_selection")
@Serializable
data class CreateWalletStart(
val mode: Mode,
) : AppRoute(path = "/create_wallet_start") {
enum class Mode {
ColdWallet,
HotWallet,
}
}
@Serializable
object CreateMobileWallet : AppRoute(path = "/create_mobile_wallet")

View file

@ -1,12 +1,54 @@
package com.tangem.core.ui.components
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.staticCompositionLocalOf
import com.google.accompanist.systemuicontroller.SystemUiController
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.tangem.core.ui.res.LocalIsInDarkTheme
val LocalSystemBarsIconsController = staticCompositionLocalOf<SystemBarsIconsController> {
error("No SystemBarsIconsController provided")
}
class SystemBarsIconsController(private val systemUiController: SystemUiController) {
private var count by mutableIntStateOf(0)
fun setIcons(darkIcons: Boolean, isNavigationBarContrastEnforced: Boolean) {
if (count == 0) {
systemUiController.systemBarsDarkContentEnabled = darkIcons
systemUiController.isNavigationBarContrastEnforced = isNavigationBarContrastEnforced
}
count++
}
fun restoreIcons(isDarkTheme: Boolean) {
count--
if (count == 0) {
systemUiController.systemBarsDarkContentEnabled = !isDarkTheme
systemUiController.isNavigationBarContrastEnforced = false
}
}
}
@Composable
fun ProvideSystemBarsIconsController(content: @Composable () -> Unit) {
val systemUiController = rememberSystemUiController()
val controller = remember(systemUiController) { SystemBarsIconsController(systemUiController) }
CompositionLocalProvider(
LocalSystemBarsIconsController provides controller,
content = content,
)
}
/**
* Provides the ability to set a scrim for 3-button navigation
*
@ -43,19 +85,16 @@ fun NavigationBar3ButtonsScrim() {
*/
@Composable
fun SystemBarsIconsDisposable(darkIcons: Boolean, isNavigationBarContrastEnforced: Boolean = false) {
val systemUiController = rememberSystemUiController()
val controller = LocalSystemBarsIconsController.current
val isDarkTheme = LocalIsInDarkTheme.current
SideEffect {
systemUiController.systemBarsDarkContentEnabled = darkIcons
systemUiController.isNavigationBarContrastEnforced = isNavigationBarContrastEnforced
controller.setIcons(darkIcons, isNavigationBarContrastEnforced)
}
val isDarkTheme = LocalIsInDarkTheme.current
DisposableEffect(isDarkTheme) {
onDispose {
systemUiController.systemBarsDarkContentEnabled = !isDarkTheme
systemUiController.isNavigationBarContrastEnforced = false
controller.restoreIcons(isDarkTheme)
}
}
}

View file

@ -13,6 +13,8 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalView
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.components.LocalSystemBarsIconsController
import com.tangem.core.ui.components.SystemBarsIconsController
import com.tangem.core.ui.components.TangemShimmer
import com.tangem.core.ui.components.text.BladeAnimation
import com.tangem.core.ui.components.text.rememberBladeAnimation
@ -65,6 +67,8 @@ fun TangemTheme(
val themeColors = if (isDark) darkThemeColors() else lightThemeColors()
val rememberedColors = remember { themeColors }
.also { it.update(themeColors) }
val systemUiController = rememberSystemUiController()
val systemBarsIconsController = remember(systemUiController) { SystemBarsIconsController(systemUiController) }
val shapes = remember { TangemShapes(dimens) }
@ -103,6 +107,7 @@ fun TangemTheme(
LocalEventMessageHandler provides eventMessageHandler,
LocalWindowSize provides windowSize,
LocalBladeAnimation provides rememberBladeAnimation(),
LocalSystemBarsIconsController provides systemBarsIconsController,
) {
CompositionLocalProvider(
LocalTangemShimmer provides TangemShimmer,
@ -119,6 +124,14 @@ fun TangemTheme(
}
}
@Composable
fun ForceDarkTheme(content: @Composable () -> Unit) {
CompositionLocalProvider(
LocalTangemColors provides darkThemeColors(),
content = content,
)
}
object TangemTheme {
val colors: TangemColors
@Composable

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="19dp"
android:height="24dp"
android:viewportWidth="19"
android:viewportHeight="24">
<path
android:pathData="M12.632,12.113C12.632,12.331 12.552,12.511 12.382,12.682L8.287,16.687C8.148,16.825 7.984,16.889 7.787,16.889C7.388,16.889 7.063,16.575 7.063,16.176C7.063,15.979 7.149,15.798 7.292,15.655L10.93,12.118L7.292,8.57C7.149,8.427 7.063,8.251 7.063,8.049C7.063,7.65 7.388,7.331 7.787,7.331C7.984,7.331 8.148,7.4 8.287,7.538L12.382,11.543C12.558,11.714 12.632,11.894 12.632,12.113Z"
android:fillColor="#ffffff"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="17dp"
android:height="16dp"
android:viewportWidth="17"
android:viewportHeight="16">
<path
android:pathData="M10.49,1.932C10.866,2.174 11.072,2.631 10.988,3.121L10.323,7.004C10.312,7.067 10.355,7.127 10.418,7.138L12.198,7.443C12.706,7.53 12.997,7.933 13.079,8.318C13.162,8.702 13.063,9.161 12.713,9.481L7.814,13.946C7.422,14.304 6.898,14.317 6.51,14.067C6.135,13.825 5.928,13.368 6.012,12.878L6.677,8.995C6.688,8.932 6.646,8.872 6.582,8.861L4.803,8.556C4.295,8.469 4.003,8.066 3.921,7.681C3.839,7.297 3.937,6.838 4.288,6.519L9.186,2.053C9.579,1.695 10.102,1.682 10.49,1.932Z"
android:fillColor="#0099FF"/>
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="16"
android:viewportHeight="16">
<path
android:pathData="M7.973,1.085C8.088,1.085 8.207,1.096 8.331,1.118C8.455,1.14 8.572,1.167 8.682,1.198C8.829,1.247 9.036,1.316 9.302,1.405C9.573,1.494 9.871,1.596 10.197,1.712C10.523,1.828 10.849,1.943 11.175,2.059C11.505,2.17 11.806,2.275 12.077,2.373C12.348,2.471 12.559,2.551 12.711,2.613C13.046,2.746 13.293,2.905 13.454,3.087C13.615,3.269 13.695,3.556 13.695,3.948V8.912C13.695,9.508 13.606,10.035 13.427,10.493C13.248,10.947 12.956,11.372 12.552,11.768C12.149,12.164 11.609,12.562 10.934,12.962C10.264,13.367 9.435,13.816 8.448,14.31C8.361,14.355 8.274,14.385 8.187,14.403C8.104,14.426 8.033,14.436 7.973,14.436C7.913,14.436 7.84,14.425 7.753,14.403C7.67,14.385 7.585,14.355 7.498,14.31C6.529,13.79 5.708,13.324 5.033,12.915C4.363,12.51 3.823,12.117 3.414,11.734C3.006,11.347 2.71,10.929 2.526,10.48C2.343,10.031 2.251,9.508 2.251,8.912V3.948C2.251,3.556 2.331,3.269 2.492,3.087C2.653,2.905 2.9,2.746 3.235,2.613C3.387,2.555 3.598,2.477 3.869,2.379C4.14,2.277 4.438,2.168 4.764,2.052C5.09,1.937 5.416,1.823 5.742,1.712C6.072,1.596 6.371,1.494 6.637,1.405C6.908,1.316 7.117,1.247 7.264,1.198C7.379,1.167 7.496,1.14 7.615,1.118C7.739,1.096 7.858,1.085 7.973,1.085ZM11.402,5.142C11.057,4.855 10.545,4.902 10.258,5.246L7.505,8.554L6.188,7.237C5.871,6.92 5.357,6.92 5.04,7.237C4.723,7.555 4.723,8.069 5.04,8.386L6.986,10.332C7.147,10.494 7.369,10.58 7.597,10.569C7.825,10.559 8.039,10.453 8.185,10.278L11.507,6.285C11.794,5.941 11.747,5.429 11.402,5.142Z"
android:fillColor="#0099FF"
android:fillType="evenOdd"/>
</vector>

View file

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="17dp"
android:height="16dp"
android:viewportWidth="17"
android:viewportHeight="16">
<path
android:pathData="M10.313,1.505C10.524,1.505 10.708,1.652 10.755,1.858L11.08,3.303C11.318,4.358 12.142,5.182 13.196,5.42L14.642,5.745C14.848,5.792 14.995,5.975 14.995,6.187C14.995,6.399 14.848,6.583 14.642,6.629L13.196,6.955C12.142,7.193 11.318,8.016 11.08,9.071L10.755,10.516C10.708,10.723 10.524,10.87 10.313,10.87C10.101,10.87 9.917,10.723 9.87,10.516L9.545,9.071C9.307,8.016 8.484,7.193 7.429,6.955L5.984,6.629C5.777,6.583 5.63,6.399 5.63,6.187C5.63,5.975 5.777,5.792 5.984,5.745L7.429,5.42C8.484,5.182 9.307,4.358 9.545,3.303L9.87,1.858C9.917,1.652 10.101,1.505 10.313,1.505Z"
android:fillColor="#0099FF"/>
<path
android:pathData="M5.479,7.547C5.691,7.547 5.874,7.694 5.921,7.9L6.154,8.933C6.312,9.637 6.863,10.188 7.567,10.346L8.6,10.579C8.806,10.625 8.953,10.809 8.953,11.021C8.953,11.233 8.806,11.416 8.6,11.463L7.567,11.696C6.863,11.854 6.312,12.405 6.154,13.109L5.921,14.142C5.874,14.348 5.691,14.495 5.479,14.495C5.267,14.495 5.083,14.348 5.037,14.142L4.804,13.109C4.646,12.405 4.095,11.854 3.391,11.696L2.358,11.463C2.152,11.416 2.005,11.233 2.005,11.021C2.005,10.809 2.152,10.625 2.358,10.579L3.391,10.346C4.095,10.188 4.646,9.637 4.804,8.933L5.037,7.9C5.083,7.694 5.267,7.547 5.479,7.547Z"
android:fillColor="#0099FF"/>
</vector>

View file

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="16"
android:viewportHeight="16">
<path
android:pathData="M12.94,10.526C13.241,10.375 13.608,10.497 13.758,10.798C13.909,11.099 13.786,11.465 13.485,11.615L8.459,14.129C8.422,14.148 8.3,14.215 8.16,14.241C8.054,14.261 7.945,14.261 7.839,14.241C7.7,14.215 7.578,14.148 7.541,14.129L2.515,11.615C2.214,11.465 2.091,11.099 2.241,10.798C2.392,10.497 2.758,10.375 3.059,10.526L8,12.995L12.94,10.526Z"
android:fillColor="#0099FF"/>
<path
android:pathData="M12.94,7.455C13.241,7.305 13.608,7.427 13.758,7.727C13.909,8.028 13.786,8.394 13.485,8.545L8.459,11.059C8.422,11.077 8.299,11.144 8.16,11.171C8.054,11.191 7.945,11.191 7.839,11.171C7.7,11.144 7.578,11.077 7.541,11.059L2.515,8.545C2.214,8.394 2.091,8.028 2.241,7.727C2.392,7.427 2.758,7.305 3.059,7.455L8,9.925L12.94,7.455Z"
android:fillColor="#0099FF"/>
<path
android:pathData="M8.08,1.749L8.16,1.76L8.261,1.786C8.355,1.817 8.431,1.857 8.459,1.871L13.485,4.385C13.692,4.488 13.822,4.7 13.823,4.93C13.823,5.161 13.691,5.372 13.485,5.475L8.459,7.989C8.422,8.008 8.299,8.074 8.16,8.1C8.054,8.12 7.945,8.12 7.839,8.1C7.7,8.074 7.578,8.008 7.541,7.989L2.514,5.475C2.308,5.372 2.177,5.161 2.177,4.93C2.177,4.7 2.308,4.488 2.514,4.385L7.541,1.871C7.578,1.853 7.7,1.786 7.839,1.76L7.92,1.749C7.973,1.744 8.026,1.744 8.08,1.749Z"
android:fillColor="#0099FF"/>
</vector>

View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,21 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
id("configuration")
}
android {
namespace = "com.tangem.features.createwalletstart.api"
}
dependencies {
/* Project - Domain */
implementation(projects.domain.models)
/* Project - Core */
implementation(projects.core.decompose)
implementation(projects.core.ui)
/* Compose */
implementation(deps.compose.runtime)
}

View file

@ -0,0 +1,18 @@
package com.tangem.features.createwalletstart
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
interface CreateWalletStartComponent : ComposableContentComponent {
data class Params(
val mode: Mode,
)
enum class Mode {
ColdWallet,
HotWallet,
}
interface Factory : ComponentFactory<Params, CreateWalletStartComponent>
}

View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,72 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.kapt)
alias(deps.plugins.kotlin.serialization)
alias(deps.plugins.hilt.android)
id("configuration")
}
android {
namespace = "com.tangem.features.createwalletstart.impl"
}
dependencies {
/** Api */
implementation(projects.features.createWalletStart.api)
/** Project - Domain */
implementation(projects.domain.card)
implementation(projects.domain.settings)
implementation(projects.domain.wallets)
implementation(projects.domain.models)
/** Core modules */
implementation(projects.core.configToggles)
implementation(projects.core.analytics)
implementation(projects.core.analytics.models)
implementation(projects.core.utils)
implementation(projects.core.ui)
implementation(projects.core.res)
implementation(projects.core.decompose)
implementation(projects.core.navigation)
implementation(projects.core.datasource)
/** Common */
implementation(projects.common.ui)
implementation(projects.common.routing)
/** Tangem libraries */
implementation(projects.libs.tangemSdkApi)
implementation(tangemDeps.card.core)
implementation(tangemDeps.card.android) {
exclude(module = "joda-time")
}
/** AndroidX libraries */
implementation(deps.androidx.core.ktx)
implementation(deps.lifecycle.runtime.ktx)
/** Compose libraries */
implementation(deps.compose.material3)
implementation(deps.compose.animation)
implementation(deps.compose.foundation)
implementation(deps.compose.ui)
implementation(deps.compose.ui.tooling)
implementation(deps.compose.coil)
implementation(deps.lottie.compose)
implementation(deps.decompose.ext.compose)
implementation(deps.androidx.activity.compose)
implementation(deps.androidx.datastore)
/** Other libraries */
implementation(deps.arrow.core)
implementation(deps.kotlin.immutable.collections)
implementation(deps.kotlin.serialization)
implementation(deps.timber)
implementation(deps.firebase.crashlytics)
/** DI */
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
}

View file

@ -0,0 +1,243 @@
package com.tangem.features.createwalletstart
import com.tangem.common.core.TangemError
import com.tangem.common.core.TangemSdkError
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic.SignedIn
import com.tangem.core.analytics.models.Basic.SignedIn.SignInType
import com.tangem.core.decompose.di.GlobalUiMessageSender
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.R
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.DialogMessage
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.analytics.ParamCardCurrencyConverter
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.common.wallets.error.SaveWalletError
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
import com.tangem.features.createwalletstart.entity.CreateWalletStartUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
private const val HIDE_PROGRESS_DELAY = 400L
@Suppress("LongParameterList")
@ModelScoped
internal class CreateWalletStartModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val router: Router,
private val scanCardProcessor: ScanCardProcessor,
private val cardSdkConfigRepository: CardSdkConfigRepository,
private val settingsRepository: SettingsRepository,
private val analyticsEventHandler: AnalyticsEventHandler,
private val appRouter: AppRouter,
private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory,
private val saveWalletUseCase: SaveWalletUseCase,
private val userWalletsListRepository: UserWalletsListRepository,
@GlobalUiMessageSender private val uiMessageSender: UiMessageSender,
private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase,
private val urlOpener: UrlOpener,
) : Model() {
private val params = paramsContainer.require<CreateWalletStartComponent.Params>()
internal val uiState: StateFlow<CreateWalletStartUM>
field = MutableStateFlow(
when (params.mode) {
CreateWalletStartComponent.Mode.ColdWallet -> CreateWalletStartUM(
title = resourceReference(R.string.common_tangem_wallet),
description = resourceReference(R.string.welcome_create_wallet_hardware_description),
featureItems = persistentListOf(
CreateWalletStartUM.FeatureItem(
iconResId = R.drawable.ic_shield_check_16,
text = resourceReference(R.string.welcome_create_wallet_feature_class),
),
CreateWalletStartUM.FeatureItem(
iconResId = R.drawable.ic_flash_16,
text = resourceReference(R.string.welcome_create_wallet_feature_delivery),
),
CreateWalletStartUM.FeatureItem(
iconResId = R.drawable.ic_sparkles_16,
text = resourceReference(R.string.welcome_create_wallet_feature_use),
),
),
imageResId = R.drawable.img_hardware_wallet,
showScanSecondaryButton = true,
onPrimaryButtonClick = ::onBuyClick,
primaryButtonText = resourceReference(R.string.details_buy_wallet),
otherMethodTitle = resourceReference(R.string.welcome_create_wallet_mobile_title),
otherMethodDescription = resourceReference(R.string.welcome_create_wallet_mobile_description),
otherMethodClick = ::onStartWithMobileWalletClick,
onBackClick = { router.pop() },
onScanClick = ::onScanClick,
isScanInProgress = false,
)
CreateWalletStartComponent.Mode.HotWallet -> CreateWalletStartUM(
title = resourceReference(R.string.hw_mobile_wallet),
description = resourceReference(R.string.welcome_create_wallet_mobile_description_full),
featureItems = persistentListOf(
CreateWalletStartUM.FeatureItem(
iconResId = R.drawable.ic_shield_check_16,
text = resourceReference(R.string.welcome_create_wallet_feature_seamless),
),
CreateWalletStartUM.FeatureItem(
iconResId = R.drawable.ic_flash_16,
text = resourceReference(R.string.welcome_create_wallet_feature_one_tap),
),
CreateWalletStartUM.FeatureItem(
iconResId = R.drawable.ic_stack_fill_new_16,
text = resourceReference(R.string.welcome_create_wallet_feature_assets),
),
),
imageResId = R.drawable.img_mobile_wallet,
showScanSecondaryButton = false,
onPrimaryButtonClick = ::onStartWithMobileWalletClick,
primaryButtonText = resourceReference(R.string.welcome_create_wallet_mobile_title),
otherMethodTitle = resourceReference(R.string.welcome_create_wallet_use_hardware_title),
otherMethodDescription = resourceReference(R.string.welcome_create_wallet_use_hardware_description),
otherMethodClick = ::onBuyClick,
onBackClick = { router.pop() },
onScanClick = ::onScanClick,
isScanInProgress = false,
)
},
)
private fun onScanClick() {
scanCard()
}
private fun onStartWithMobileWalletClick() {
router.push(AppRoute.CreateMobileWallet)
}
private fun onBuyClick() {
modelScope.launch {
generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) }
}
}
private fun scanCard() {
modelScope.launch {
setLoading(true)
val shouldSaveAccessCodes = settingsRepository.shouldSaveAccessCodes()
cardSdkConfigRepository.setAccessCodeRequestPolicy(
isBiometricsRequestPolicy = shouldSaveAccessCodes,
)
val analyticsSource = AnalyticsParam.ScreensSources.Intro
scanCardProcessor.scan(
analyticsSource = analyticsSource,
onProgressStateChange = { showProgress ->
if (!showProgress) {
delay(HIDE_PROGRESS_DELAY)
setLoading(false)
} else {
setLoading(true)
}
},
onFailure = { error ->
handleScanError(error)
delay(HIDE_PROGRESS_DELAY)
setLoading(false)
},
onSuccess = { scanResponse ->
proceedWithScanResponse(scanResponse)
},
)
}
}
private suspend fun proceedWithScanResponse(scanResponse: ScanResponse) {
val userWallet = coldUserWalletBuilderFactory.create(scanResponse = scanResponse).build()
if (userWallet == null) {
Timber.e("User wallet not created")
setLoading(false)
return
}
saveWalletUseCase(userWallet = userWallet).fold(
ifLeft = {
delay(HIDE_PROGRESS_DELAY)
setLoading(false)
when (it) {
is SaveWalletError.DataError -> Timber.e(it.toString(), "Unable to save user wallet")
is SaveWalletError.WalletAlreadySaved -> {
userWalletsListRepository.unlock(
userWalletId = userWallet.walletId,
unlockMethod = UserWalletsListRepository.UnlockMethod.Scan(scanResponse),
).onRight {
appRouter.replaceAll(AppRoute.Wallet)
}
}
}
},
ifRight = {
setLoading(false)
sendSignedInCardAnalyticsEvent(scanResponse)
appRouter.replaceAll(AppRoute.Wallet)
},
)
}
private suspend fun sendSignedInCardAnalyticsEvent(scanResponse: ScanResponse) {
val currency = ParamCardCurrencyConverter().convert(value = scanResponse.cardTypesResolver)
if (currency != null) {
analyticsEventHandler.send(
SignedIn(
currency = currency,
batch = scanResponse.card.batchId,
signInType = SignInType.Card,
walletsCount = userWalletsListRepository.userWalletsSync().size.toString(),
hasBackup = scanResponse.card.backupStatus?.isActive,
),
)
}
}
private fun setLoading(isLoading: Boolean) {
uiState.update { it.copy(isScanInProgress = isLoading) }
}
private fun handleScanError(error: TangemError) {
when (error) {
is TangemSdkError.NfcFeatureIsUnavailable -> handleNfcFeatureUnavailable()
is TangemSdkError -> Timber.e(error, "Scan error occurred")
else -> Timber.e(error, "Error happened")
}
}
private fun handleNfcFeatureUnavailable() {
uiMessageSender.send(
message = DialogMessage(
message = resourceReference(R.string.nfc_error_unavailable),
title = resourceReference(id = R.string.common_error),
),
)
}
}

View file

@ -0,0 +1,42 @@
package com.tangem.features.createwalletstart
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
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.components.SystemBarsIconsDisposable
import com.tangem.core.ui.res.ForceDarkTheme
import com.tangem.features.createwalletstart.ui.CreateWalletStartContent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class DefaultCreateWalletStartComponent @AssistedInject constructor(
@Assisted private val context: AppComponentContext,
@Assisted private val params: CreateWalletStartComponent.Params,
) : CreateWalletStartComponent, AppComponentContext by context {
private val model: CreateWalletStartModel = getOrCreateModel(params)
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
SystemBarsIconsDisposable(darkIcons = false)
ForceDarkTheme {
CreateWalletStartContent(
state = state,
modifier = modifier,
)
}
}
@AssistedFactory
interface Factory : CreateWalletStartComponent.Factory {
override fun create(
context: AppComponentContext,
params: CreateWalletStartComponent.Params,
): DefaultCreateWalletStartComponent
}
}

View file

@ -0,0 +1,33 @@
package com.tangem.features.createwalletstart.di
import com.tangem.core.decompose.model.Model
import com.tangem.features.createwalletstart.CreateWalletStartComponent
import com.tangem.features.createwalletstart.CreateWalletStartModel
import com.tangem.features.createwalletstart.DefaultCreateWalletStartComponent
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object CreateWalletStartModule
@Module
@InstallIn(SingletonComponent::class)
internal interface CreateWalletStartModuleBinds {
@Binds
@Singleton
fun bindCreateWalletStartComponentFactory(
impl: DefaultCreateWalletStartComponent.Factory,
): CreateWalletStartComponent.Factory
@Binds
@IntoMap
@ClassKey(CreateWalletStartModel::class)
fun bindCreateWalletStartModel(model: CreateWalletStartModel): Model
}

View file

@ -0,0 +1,25 @@
package com.tangem.features.createwalletstart.entity
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
internal data class CreateWalletStartUM(
val title: TextReference,
val description: TextReference,
val featureItems: ImmutableList<FeatureItem>,
val imageResId: Int,
val isScanInProgress: Boolean,
val showScanSecondaryButton: Boolean,
val primaryButtonText: TextReference,
val onPrimaryButtonClick: () -> Unit,
val otherMethodDescription: TextReference,
val otherMethodTitle: TextReference,
val otherMethodClick: () -> Unit,
val onScanClick: () -> Unit,
val onBackClick: () -> Unit,
) {
data class FeatureItem(
val iconResId: Int,
val text: TextReference,
)
}

View file

@ -0,0 +1,486 @@
package com.tangem.features.createwalletstart.ui
import android.annotation.SuppressLint
import android.content.res.Configuration
import androidx.annotation.DrawableRes
import androidx.compose.foundation.*
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.SecondaryButtonIconEnd
import com.tangem.core.ui.components.bottomFade
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.createwalletstart.entity.CreateWalletStartUM
import com.tangem.features.createwalletstart.impl.R
import kotlinx.collections.immutable.persistentListOf
import kotlin.math.max
@Suppress("LongMethod", "MagicNumber")
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
@Composable
internal fun CreateWalletStartContent(state: CreateWalletStartUM, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.background(
brush = Brush.verticalGradient(
listOf(
TangemColorPalette.Dark6,
TangemColorPalette.Black,
),
),
)
.fillMaxSize()
.systemBarsPadding(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
TopAppBar(
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Color.Transparent,
),
navigationIcon = {
IconButton(onClick = state.onBackClick) {
Icon(
painter = painterResource(R.drawable.ic_back_24),
tint = TangemTheme.colors.icon.primary1,
contentDescription = null,
)
}
},
title = { },
)
Box(
modifier = Modifier
.weight(1f)
.bottomFade(height = 24.dp),
) {
AdaptiveScrollableContent(
topContent = {
Text(
modifier = Modifier
.fillMaxWidth()
.padding(
start = 32.dp,
top = 16.dp,
end = 32.dp,
),
text = state.title.resolveReference(),
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
)
Text(
modifier = Modifier
.fillMaxWidth()
.padding(
start = 32.dp,
top = 8.dp,
end = 32.dp,
),
text = state.description.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
)
FlowRow(
modifier = Modifier
.fillMaxWidth()
.padding(
start = 24.dp,
top = 16.dp,
end = 24.dp,
),
horizontalArrangement = Arrangement.Center,
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
state.featureItems.forEach {
FeatureItem(
iconResId = it.iconResId,
text = it.text,
)
}
}
},
imageContent = {
Image(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(
vertical = 12.dp,
horizontal = 16.dp,
),
painter = painterResource(id = state.imageResId),
contentDescription = null,
contentScale = ContentScale.Fit,
)
},
bottomContent = {
if (state.showScanSecondaryButton) {
SecondaryButtonIconEnd(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
text = stringResourceSafe(R.string.welcome_unlock_card),
onClick = state.onScanClick,
showProgress = state.isScanInProgress,
iconResId = R.drawable.ic_tangem_24,
)
}
PrimaryButton(
modifier = Modifier
.fillMaxWidth()
.padding(
start = 16.dp,
top = 8.dp,
end = 16.dp,
),
text = state.primaryButtonText.resolveReference(),
onClick = state.onPrimaryButtonClick,
)
Row(
modifier = Modifier
.padding(
start = 16.dp,
top = 24.dp,
end = 16.dp,
),
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
DashedGradientLine(
modifier = Modifier
.weight(1f)
.height(16.dp),
)
Text(
text = stringResourceSafe(R.string.welcome_create_wallet_other_method),
style = TangemTheme.typography.caption1,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
)
DashedGradientLine(
modifier = Modifier
.weight(1f)
.height(16.dp)
.scale(scaleX = -1f, scaleY = 1f),
)
}
Text(
modifier = Modifier
.fillMaxWidth()
.padding(
start = 16.dp,
top = 16.dp,
end = 16.dp,
),
text = state.otherMethodDescription.resolveReference(),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
textAlign = TextAlign.Center,
)
Row(
modifier = Modifier
.wrapContentWidth()
.clickable { state.otherMethodClick() }
.padding(
horizontal = 16.dp,
vertical = 12.dp,
),
horizontalArrangement = Arrangement.Center,
) {
Text(
text = state.otherMethodTitle.resolveReference(),
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
)
Icon(
painter = painterResource(id = R.drawable.ic_chevron_right_18x24),
tint = TangemTheme.colors.icon.primary1,
contentDescription = null,
)
}
},
minImageHeight = 160.dp,
)
}
if (!state.showScanSecondaryButton) {
FlowRow(
modifier = Modifier
.wrapContentWidth()
.padding(
start = 16.dp,
top = 24.dp,
end = 16.dp,
bottom = 8.dp,
),
horizontalArrangement = Arrangement.Center,
) {
Text(
text = stringResourceSafe(R.string.welcome_create_wallet_already_have),
style = TangemTheme.typography.caption1,
color = TangemTheme.colors.text.secondary,
)
Spacer(modifier = Modifier.size(4.dp))
Row(
modifier = Modifier
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
) { state.onScanClick() },
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = stringResourceSafe(R.string.wallet_create_scan_title),
style = TangemTheme.typography.caption1,
color = TangemTheme.colors.text.primary1,
)
Spacer(modifier = Modifier.size(2.dp))
Icon(
modifier = Modifier.size(16.dp),
painter = painterResource(id = R.drawable.ic_tangem_24),
tint = TangemTheme.colors.icon.primary1,
contentDescription = null,
)
}
}
}
Spacer(modifier = Modifier.size(16.dp))
}
}
@SuppressLint("UnusedBoxWithConstraintsScope")
@Composable
private fun AdaptiveScrollableContent(
minImageHeight: Dp,
modifier: Modifier = Modifier,
topContent: @Composable () -> Unit,
imageContent: @Composable () -> Unit,
bottomContent: @Composable () -> Unit,
) {
BoxWithConstraints(
modifier = modifier.fillMaxSize(),
) {
val density = LocalDensity.current
val viewportHeight = maxHeight
val minImageHeightPx = with(density) { minImageHeight.roundToPx() }
val viewportHeightPx = with(density) { viewportHeight.roundToPx() }
Layout(
modifier = Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState()),
content = {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
topContent()
}
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.Center,
) {
imageContent()
}
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
bottomContent()
}
},
) { measurables, constraints ->
val topPlaceable = measurables[0].measure(
constraints.copy(minHeight = 0, maxHeight = androidx.compose.ui.unit.Constraints.Infinity),
)
val bottomPlaceable = measurables[2].measure(
constraints.copy(minHeight = 0, maxHeight = androidx.compose.ui.unit.Constraints.Infinity),
)
val imageIntrinsicHeight = measurables[1].maxIntrinsicHeight(constraints.maxWidth)
val availableHeightForImage = max(0, viewportHeightPx - topPlaceable.height - bottomPlaceable.height)
val targetImageHeight = when {
imageIntrinsicHeight < minImageHeightPx -> minImageHeightPx
imageIntrinsicHeight > availableHeightForImage -> max(minImageHeightPx, availableHeightForImage)
else -> imageIntrinsicHeight
}
val imagePlaceable = measurables[1].measure(
constraints.copy(
minHeight = targetImageHeight,
maxHeight = targetImageHeight,
),
)
val totalContentHeight = topPlaceable.height + imagePlaceable.height + bottomPlaceable.height
layout(constraints.maxWidth, totalContentHeight) {
var yOffset = 0
topPlaceable.placeRelative(0, yOffset)
yOffset += topPlaceable.height
imagePlaceable.placeRelative(0, yOffset)
yOffset += imagePlaceable.height
bottomPlaceable.placeRelative(0, yOffset)
}
}
}
}
@Composable
private fun DashedGradientLine(modifier: Modifier = Modifier) {
val density = LocalDensity.current
val strokeColor = TangemTheme.colors.stroke.primary
Canvas(modifier = modifier) {
val strokePx = with(density) { 4.dp.toPx() }
val dashPx = with(density) { 4.dp.toPx() }
val gapPx = with(density) { 8.dp.toPx() }
val width = size.width
val centerY = size.height / 2
val brush = Brush.linearGradient(
colors = listOf(strokeColor.copy(alpha = 0f), strokeColor),
start = Offset(0f, 0f),
end = Offset(width, 0f),
)
val pathEffect = PathEffect.dashPathEffect(floatArrayOf(dashPx, gapPx), 0f)
drawLine(
brush = brush,
start = Offset(0f, centerY),
end = Offset(width, centerY),
strokeWidth = strokePx,
pathEffect = pathEffect,
cap = StrokeCap.Round,
)
}
}
@Composable
private fun FeatureItem(@DrawableRes iconResId: Int, text: TextReference) {
Row(
modifier = Modifier
.wrapContentWidth()
.padding(horizontal = 8.dp),
horizontalArrangement = Arrangement.spacedBy(6.dp),
) {
Icon(
modifier = Modifier.size(16.dp),
painter = painterResource(iconResId),
tint = TangemTheme.colors.icon.accent,
contentDescription = null,
)
Text(
text = text.resolveReference(),
style = TangemTheme.typography.caption1,
color = TangemTheme.colors.text.secondary,
)
}
}
private class CreateWalletStartStateProvider : CollectionPreviewParameterProvider<CreateWalletStartUM>(
collection = listOf(
CreateWalletStartUM(
title = resourceReference(R.string.common_tangem_wallet),
description = resourceReference(R.string.welcome_create_wallet_hardware_description),
featureItems = persistentListOf(
CreateWalletStartUM.FeatureItem(
iconResId = R.drawable.ic_shield_check_16,
text = resourceReference(R.string.welcome_create_wallet_feature_class),
),
CreateWalletStartUM.FeatureItem(
iconResId = R.drawable.ic_flash_16,
text = resourceReference(R.string.welcome_create_wallet_feature_delivery),
),
CreateWalletStartUM.FeatureItem(
iconResId = R.drawable.ic_sparkles_16,
text = resourceReference(R.string.welcome_create_wallet_feature_use),
),
),
imageResId = R.drawable.img_hardware_wallet,
showScanSecondaryButton = true,
onPrimaryButtonClick = { },
primaryButtonText = resourceReference(R.string.details_buy_wallet),
otherMethodTitle = resourceReference(R.string.welcome_create_wallet_mobile_title),
otherMethodDescription = resourceReference(
R.string.welcome_create_wallet_mobile_description,
),
otherMethodClick = { },
onBackClick = { },
onScanClick = { },
isScanInProgress = false,
),
CreateWalletStartUM(
title = resourceReference(R.string.hw_mobile_wallet),
description = resourceReference(R.string.welcome_create_wallet_mobile_description_full),
featureItems = persistentListOf(
CreateWalletStartUM.FeatureItem(
iconResId = R.drawable.ic_shield_check_16,
text = resourceReference(R.string.welcome_create_wallet_feature_seamless),
),
CreateWalletStartUM.FeatureItem(
iconResId = R.drawable.ic_flash_16,
text = resourceReference(R.string.welcome_create_wallet_feature_one_tap),
),
CreateWalletStartUM.FeatureItem(
iconResId = R.drawable.ic_stack_fill_new_16,
text = resourceReference(R.string.welcome_create_wallet_feature_assets),
),
),
imageResId = R.drawable.img_mobile_wallet,
showScanSecondaryButton = false,
onPrimaryButtonClick = { },
primaryButtonText = resourceReference(R.string.welcome_create_wallet_mobile_title),
otherMethodTitle = resourceReference(R.string.welcome_create_wallet_use_hardware_title),
otherMethodDescription = resourceReference(
R.string.welcome_create_wallet_use_hardware_description,
),
otherMethodClick = { },
onBackClick = { },
onScanClick = { },
isScanInProgress = false,
),
),
)
@Preview(showBackground = true, widthDp = 360, heightDp = 480, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(showBackground = true, widthDp = 360, heightDp = 560, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(showBackground = true, widthDp = 360, heightDp = 720, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(showBackground = true, widthDp = 360, heightDp = 840, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PreviewCreateWalletStartContent(
@PreviewParameter(CreateWalletStartStateProvider::class) param: CreateWalletStartUM,
) {
TangemThemePreview {
CreateWalletStartContent(
state = param,
)
}
}

View file

@ -279,6 +279,9 @@ include(":features:tangempay:onboarding:impl")
include(":features:create-wallet-selection:api")
include(":features:create-wallet-selection:impl")
include(":features:create-wallet-start:api")
include(":features:create-wallet-start:impl")
include(":features:welcome:api")
include(":features:welcome:impl")