Updated on 2026-08-14
This commit is contained in:
parent
499351d5a9
commit
2bc8c29f6d
31 changed files with 1086 additions and 7 deletions
1
features/create-wallet-start/impl/.gitignore
vendored
Normal file
1
features/create-wallet-start/impl/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
72
features/create-wallet-start/impl/build.gradle.kts
Normal file
72
features/create-wallet-start/impl/build.gradle.kts
Normal 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)
|
||||
}
|
||||
|
|
@ -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),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue