Updated on 2026-08-14
This commit is contained in:
commit
438126dc82
665 changed files with 16174 additions and 6245 deletions
|
|
@ -25,7 +25,7 @@ import com.tangem.core.ui.decompose.ComposableContentComponent
|
|||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.test.TestTags
|
||||
import com.tangem.core.ui.test.DetailsScreenTestTags
|
||||
import com.tangem.features.details.component.preview.PreviewDetailsComponent
|
||||
import com.tangem.features.details.entity.DetailsFooterUM
|
||||
import com.tangem.features.details.entity.DetailsItemUM
|
||||
|
|
@ -67,7 +67,7 @@ private fun Content(
|
|||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = modifier.testTag(TestTags.DETAILS_SCREEN),
|
||||
modifier = modifier.testTag(DetailsScreenTestTags.SCREEN_CONTAINER),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16),
|
||||
contentPadding = PaddingValues(
|
||||
top = TangemTheme.dimens.spacing12,
|
||||
|
|
@ -120,7 +120,7 @@ private fun Block(
|
|||
) {
|
||||
val itemModifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.testTag(TestTags.DETAILS_SCREEN_ITEM)
|
||||
.testTag(DetailsScreenTestTags.SCREEN_ITEM)
|
||||
|
||||
when (model) {
|
||||
is DetailsItemUM.Basic -> {
|
||||
|
|
|
|||
|
|
@ -5,10 +5,8 @@ import com.tangem.core.decompose.di.ModelScoped
|
|||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.core.ui.components.block.model.BlockUM
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.features.details.entity.DetailsItemUM
|
||||
import com.tangem.features.details.impl.BuildConfig
|
||||
import com.tangem.features.details.impl.R
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
|
@ -76,17 +74,6 @@ internal class ItemsBuilder @Inject constructor(private val router: Router) {
|
|||
onClick = { router.push(AppRoute.AppSettings) },
|
||||
),
|
||||
).let(::add)
|
||||
|
||||
if (BuildConfig.TESTER_MENU_ENABLED) {
|
||||
DetailsItemUM.Basic.Item(
|
||||
id = "tester_menu",
|
||||
block = BlockUM(
|
||||
text = stringReference(value = "Tester menu"),
|
||||
iconRes = R.drawable.ic_alert_24,
|
||||
onClick = { router.push(AppRoute.TesterMenu) },
|
||||
),
|
||||
).let(::add)
|
||||
}
|
||||
}.toImmutableList(),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -32,8 +32,7 @@ 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.core.ui.test.TestTags.DISCLAIMER_SCREEN_ACCEPT_BUTTON
|
||||
import com.tangem.core.ui.test.TestTags.DISCLAIMER_SCREEN_CONTAINER
|
||||
import com.tangem.core.ui.test.DisclaimerScreenTestTags
|
||||
import com.tangem.core.ui.webview.applySafeSettings
|
||||
import com.tangem.features.disclaimer.impl.R
|
||||
import com.tangem.features.disclaimer.impl.entity.DisclaimerUM
|
||||
|
|
@ -58,7 +57,7 @@ internal fun DisclaimerScreen(state: DisclaimerUM) {
|
|||
modifier = Modifier
|
||||
.background(backgroundColor)
|
||||
.statusBarsPadding()
|
||||
.testTag(DISCLAIMER_SCREEN_CONTAINER),
|
||||
.testTag(DisclaimerScreenTestTags.SCREEN_CONTAINER),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
|
|
@ -183,7 +182,7 @@ private fun BoxScope.DisclaimerButton(onAccept: (Boolean) -> Unit) {
|
|||
disabledContentColor = TangemColorPalette.Dark6,
|
||||
),
|
||||
modifier = Modifier
|
||||
.testTag(DISCLAIMER_SCREEN_ACCEPT_BUTTON)
|
||||
.testTag(DisclaimerScreenTestTags.ACCEPT_BUTTON)
|
||||
.align(Alignment.BottomCenter)
|
||||
.navigationBarsPadding()
|
||||
.padding(
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ android {
|
|||
dependencies {
|
||||
/** Api */
|
||||
implementation(projects.features.hotWallet.api)
|
||||
implementation(projects.features.pushNotifications.api)
|
||||
|
||||
/** Core modules */
|
||||
implementation(projects.core.configToggles)
|
||||
|
|
@ -26,6 +27,10 @@ dependencies {
|
|||
implementation(projects.core.navigation)
|
||||
implementation(projects.core.datasource)
|
||||
|
||||
/** Domain */
|
||||
implementation(projects.domain.wallets)
|
||||
implementation(projects.domain.wallets.models)
|
||||
|
||||
/** Common */
|
||||
implementation(projects.common.ui)
|
||||
implementation(projects.common.routing)
|
||||
|
|
@ -36,6 +41,8 @@ dependencies {
|
|||
implementation(tangemDeps.card.android) {
|
||||
exclude(module = "joda-time")
|
||||
}
|
||||
implementation(tangemDeps.hot.core)
|
||||
implementation(tangemDeps.hot.android)
|
||||
|
||||
/** AndroidX libraries */
|
||||
implementation(deps.androidx.core.ktx)
|
||||
|
|
|
|||
|
|
@ -6,9 +6,11 @@ import com.arkivanov.decompose.router.stack.push
|
|||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.features.hotwallet.setaccesscode.SetAccessCodeComponent
|
||||
import com.tangem.features.hotwallet.addexistingwallet.start.AddExistingWalletStartComponent
|
||||
import com.tangem.features.hotwallet.addexistingwallet.im.port.AddExistingWalletImportComponent
|
||||
import com.tangem.features.hotwallet.addexistingwallet.root.routing.AddExistingWalletRoute
|
||||
import com.tangem.features.pushnotifications.api.PushNotificationsComponent
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -20,6 +22,8 @@ internal class AddExistingWalletModel @Inject constructor(
|
|||
|
||||
val addExistingWalletStartModelCallbacks = AddExistingWalletStartModelCallbacks()
|
||||
val addExistingWalletImportModelCallbacks = AddExistingWalletImportModelCallbacks()
|
||||
val pushNotificationsComponentModelCallbacks = PushNotificationsComponentModelCallbacks()
|
||||
val accessCodeModelCallbacks = AccessCodeModelCallbacks()
|
||||
|
||||
val stackNavigation = StackNavigation<AddExistingWalletRoute>()
|
||||
|
||||
|
|
@ -38,4 +42,20 @@ internal class AddExistingWalletModel @Inject constructor(
|
|||
stackNavigation.pop()
|
||||
}
|
||||
}
|
||||
|
||||
inner class PushNotificationsComponentModelCallbacks : PushNotificationsComponent.ModelCallbacks {
|
||||
override fun onResult() {
|
||||
// TODO [REDACTED_TASK_KEY]
|
||||
}
|
||||
}
|
||||
|
||||
inner class AccessCodeModelCallbacks : SetAccessCodeComponent.ModelCallbacks {
|
||||
override fun onBackClick() {
|
||||
// TODO [REDACTED_TASK_KEY]
|
||||
}
|
||||
|
||||
override fun onAccessCodeSet() {
|
||||
// TODO [REDACTED_TASK_KEY]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.tangem.features.hotwallet.addexistingwallet.root.di
|
|||
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.features.hotwallet.AddExistingWalletComponent
|
||||
import com.tangem.features.hotwallet.setaccesscode.SetAccessCodeModel
|
||||
import com.tangem.features.hotwallet.addexistingwallet.root.AddExistingWalletModel
|
||||
import com.tangem.features.hotwallet.addexistingwallet.root.DefaultAddExistingWalletComponent
|
||||
import com.tangem.features.hotwallet.addexistingwallet.start.AddExistingWalletStartModel
|
||||
|
|
@ -42,4 +43,9 @@ internal interface AddExistingWalletModuleBinds {
|
|||
@IntoMap
|
||||
@ClassKey(AddExistingWalletImportModel::class)
|
||||
fun bindAddExistingWalletImportModel(model: AddExistingWalletImportModel): Model
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(SetAccessCodeModel::class)
|
||||
fun bindAccessCodeModel(model: SetAccessCodeModel): Model
|
||||
}
|
||||
|
|
@ -2,12 +2,16 @@ package com.tangem.features.hotwallet.addexistingwallet.root.routing
|
|||
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.features.hotwallet.setaccesscode.SetAccessCodeComponent
|
||||
import com.tangem.features.hotwallet.addexistingwallet.root.AddExistingWalletModel
|
||||
import com.tangem.features.hotwallet.addexistingwallet.start.AddExistingWalletStartComponent
|
||||
import com.tangem.features.hotwallet.addexistingwallet.im.port.AddExistingWalletImportComponent
|
||||
import com.tangem.features.pushnotifications.api.PushNotificationsComponent
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class AddExistingWalletChildFactory @Inject constructor() {
|
||||
internal class AddExistingWalletChildFactory @Inject constructor(
|
||||
private val pushNotificationsComponent: PushNotificationsComponent.Factory,
|
||||
) {
|
||||
|
||||
fun createChild(
|
||||
route: AddExistingWalletRoute,
|
||||
|
|
@ -27,6 +31,18 @@ internal class AddExistingWalletChildFactory @Inject constructor() {
|
|||
callbacks = model.addExistingWalletImportModelCallbacks,
|
||||
),
|
||||
)
|
||||
is AddExistingWalletRoute.PushNotifications -> pushNotificationsComponent.create(
|
||||
context = childContext,
|
||||
params = PushNotificationsComponent.Params.Callbacks(
|
||||
callbacks = model.pushNotificationsComponentModelCallbacks,
|
||||
),
|
||||
)
|
||||
is AddExistingWalletRoute.AccessCode -> SetAccessCodeComponent(
|
||||
context = childContext,
|
||||
params = SetAccessCodeComponent.Params(
|
||||
callbacks = model.accessCodeModelCallbacks,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -10,4 +10,10 @@ internal sealed class AddExistingWalletRoute : Route {
|
|||
|
||||
@Serializable
|
||||
object Import : AddExistingWalletRoute()
|
||||
|
||||
@Serializable
|
||||
object PushNotifications : AddExistingWalletRoute()
|
||||
|
||||
@Serializable
|
||||
object AccessCode : AddExistingWalletRoute()
|
||||
}
|
||||
|
|
@ -1,18 +1,30 @@
|
|||
package com.tangem.features.hotwallet.createmobilewallet
|
||||
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.domain.wallets.builder.HotUserWalletBuilder
|
||||
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
|
||||
import com.tangem.features.hotwallet.createmobilewallet.entity.CreateMobileWalletUM
|
||||
import com.tangem.hot.sdk.TangemHotSdk
|
||||
import com.tangem.hot.sdk.model.HotAuth
|
||||
import com.tangem.hot.sdk.model.MnemonicType
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
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
|
||||
|
||||
@ModelScoped
|
||||
internal class CreateMobileWalletModel @Inject constructor(
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val hotUserWalletBuilderFactory: HotUserWalletBuilder.Factory,
|
||||
private val saveUserWalletUseCase: SaveWalletUseCase,
|
||||
private val router: Router,
|
||||
private val tangemHotSdk: TangemHotSdk,
|
||||
) : Model() {
|
||||
|
||||
internal val uiState: StateFlow<CreateMobileWalletUM>
|
||||
|
|
@ -20,10 +32,30 @@ internal class CreateMobileWalletModel @Inject constructor(
|
|||
CreateMobileWalletUM(
|
||||
onBackClick = { router.pop() },
|
||||
onCreateClick = ::onCreateClick,
|
||||
createButtonLoading = false,
|
||||
),
|
||||
)
|
||||
|
||||
private fun onCreateClick() {
|
||||
// TODO create a wallet
|
||||
modelScope.launch {
|
||||
uiState.update {
|
||||
it.copy(createButtonLoading = true)
|
||||
}
|
||||
|
||||
runCatching {
|
||||
val hotWalletId = tangemHotSdk.generateWallet(HotAuth.NoAuth, mnemonicType = MnemonicType.Words12)
|
||||
val hotUserWalletBuilder = hotUserWalletBuilderFactory.create(hotWalletId)
|
||||
saveUserWalletUseCase(
|
||||
hotUserWalletBuilder.build(),
|
||||
)
|
||||
router.push(AppRoute.Wallet)
|
||||
}.onFailure {
|
||||
Timber.e(it)
|
||||
|
||||
uiState.update {
|
||||
it.copy(createButtonLoading = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -12,10 +12,12 @@ import dagger.assisted.Assisted
|
|||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
internal class DefaultCreateMobileWalletComponent @AssistedInject constructor(
|
||||
@Assisted private val context: AppComponentContext,
|
||||
@Assisted private val params: Unit,
|
||||
) : CreateMobileWalletComponent, AppComponentContext by context {
|
||||
|
||||
private val model: CreateMobileWalletModel = getOrCreateModel(params)
|
||||
|
||||
@Composable
|
||||
|
|
|
|||
|
|
@ -10,18 +10,12 @@ 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 CreateMobileWalletModule
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface CreateMobileWalletModuleBinds {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindCreateMobileWalletComponentFactory(
|
||||
impl: DefaultCreateMobileWalletComponent.Factory,
|
||||
): CreateMobileWalletComponent.Factory
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.features.hotwallet.createmobilewallet.entity
|
||||
|
||||
internal data class CreateMobileWalletUM(
|
||||
val createButtonLoading: Boolean,
|
||||
val onBackClick: () -> Unit,
|
||||
val onCreateClick: () -> Unit,
|
||||
)
|
||||
|
|
@ -84,8 +84,8 @@ internal fun CreateMobileWalletContent(state: CreateMobileWalletUM, modifier: Mo
|
|||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
text = "Create",
|
||||
showProgress = false,
|
||||
text = stringResourceSafe(R.string.common_create),
|
||||
showProgress = state.createButtonLoading,
|
||||
enabled = true,
|
||||
onClick = state.onCreateClick,
|
||||
)
|
||||
|
|
@ -133,6 +133,7 @@ private fun PreviewCreateWalletContent() {
|
|||
CreateMobileWalletContent(
|
||||
state = CreateMobileWalletUM(
|
||||
onBackClick = {},
|
||||
createButtonLoading = false,
|
||||
onCreateClick = {},
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
package com.tangem.features.hotwallet.manualbackup.check
|
||||
|
||||
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.decompose.ComposableContentComponent
|
||||
import com.tangem.crypto.bip39.Mnemonic
|
||||
import com.tangem.features.hotwallet.manualbackup.check.model.ManualBackupCheckModel
|
||||
import com.tangem.features.hotwallet.manualbackup.check.ui.ManualBackupCheckContent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
internal class ManualBackupCheckComponent @AssistedInject constructor(
|
||||
@Assisted private val context: AppComponentContext,
|
||||
@Assisted private val params: Params,
|
||||
) : ComposableContentComponent, AppComponentContext by context {
|
||||
private val model: ManualBackupCheckModel = getOrCreateModel(params)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
ManualBackupCheckContent(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
interface ModelCallbacks {
|
||||
fun onCompleteClick()
|
||||
}
|
||||
|
||||
data class Params(
|
||||
val generatedWords: Mnemonic,
|
||||
val callbacks: ModelCallbacks,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.features.hotwallet.manualbackup.check.entity
|
||||
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
internal data class ManualBackupCheckUM(
|
||||
val onCompleteButtonClick: () -> Unit,
|
||||
val wordFields: ImmutableList<WordField>,
|
||||
val completeButtonEnabled: Boolean,
|
||||
val completeButtonProgress: Boolean,
|
||||
) {
|
||||
data class WordField(
|
||||
val index: Int,
|
||||
val word: TextFieldValue,
|
||||
val error: Boolean,
|
||||
val onChange: (TextFieldValue) -> Unit,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
package com.tangem.features.hotwallet.manualbackup.check.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.features.hotwallet.manualbackup.check.ManualBackupCheckComponent
|
||||
import com.tangem.features.hotwallet.manualbackup.check.entity.ManualBackupCheckUM
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import javax.inject.Inject
|
||||
import kotlin.Boolean
|
||||
import kotlin.Int
|
||||
import kotlin.String
|
||||
import kotlin.Suppress
|
||||
import kotlin.collections.List
|
||||
import kotlin.collections.all
|
||||
import kotlin.collections.map
|
||||
import kotlin.error
|
||||
|
||||
@Stable
|
||||
@ModelScoped
|
||||
internal class ManualBackupCheckModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<ManualBackupCheckComponent.Params>()
|
||||
private val callbacks = params.callbacks
|
||||
|
||||
internal val uiState: StateFlow<ManualBackupCheckUM>
|
||||
field = MutableStateFlow(getInitialUIState())
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private fun getInitialUIState(): ManualBackupCheckUM {
|
||||
val wordFields = List(WORD_FIELD_INDICES.size) { index ->
|
||||
val shownIndex = WORD_FIELD_INDICES[index]
|
||||
|
||||
ManualBackupCheckUM.WordField(
|
||||
index = shownIndex,
|
||||
word = TextFieldValue(""),
|
||||
error = false,
|
||||
onChange = { textFieldValue ->
|
||||
updateWordField(shownIndex, textFieldValue)
|
||||
},
|
||||
)
|
||||
}.toImmutableList()
|
||||
|
||||
return ManualBackupCheckUM(
|
||||
wordFields = wordFields,
|
||||
completeButtonEnabled = false,
|
||||
completeButtonProgress = false,
|
||||
onCompleteButtonClick = {
|
||||
val currentUIState = uiState.value
|
||||
if (currentUIState.completeButtonEnabled) {
|
||||
callbacks.onCompleteClick()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateWordField(shownIndex: Int, newText: TextFieldValue) {
|
||||
uiState.update { currentState ->
|
||||
val updatedFields = currentState.wordFields.map { wordField ->
|
||||
if (wordField.index == shownIndex) {
|
||||
val isCorrect = checkWordField(newText.text, shownIndex)
|
||||
wordField.copy(
|
||||
word = newText,
|
||||
error = !isCorrect,
|
||||
)
|
||||
} else {
|
||||
wordField
|
||||
}
|
||||
}.toImmutableList()
|
||||
|
||||
val allFieldsCorrect = updatedFields.all { field ->
|
||||
checkWordField(field.word.text, field.index)
|
||||
}
|
||||
|
||||
currentState.copy(
|
||||
wordFields = updatedFields,
|
||||
completeButtonEnabled = allFieldsCorrect,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkWordField(word: String, shownIndex: Int): Boolean {
|
||||
val generatedWords = params.generatedWords
|
||||
val wordList = generatedWords.mnemonicComponents
|
||||
|
||||
return if (shownIndex <= wordList.size) {
|
||||
wordList[shownIndex - 1] == word
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val WORD_FIELD_INDICES = listOf(2, 7, 11)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
package com.tangem.features.hotwallet.manualbackup.check.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusDirection
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEachIndexed
|
||||
import com.tangem.core.ui.components.OutlineTextField
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.components.fields.contextmenu.DisableContextMenu
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.hotwallet.impl.R
|
||||
import com.tangem.features.hotwallet.manualbackup.check.entity.ManualBackupCheckUM
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@Composable
|
||||
internal fun ManualBackupCheckContent(state: ManualBackupCheckUM, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.fillMaxSize()
|
||||
.imePadding(),
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
.verticalScroll(rememberScrollState())
|
||||
.weight(1f),
|
||||
) {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.onboarding_seed_user_validation_title),
|
||||
style = TangemTheme.typography.h2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.padding(top = 48.dp, start = 36.dp, end = 36.dp, bottom = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.onboarding_seed_user_validation_message),
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 36.dp)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
|
||||
Fields(
|
||||
state = state,
|
||||
modifier = Modifier
|
||||
.padding(vertical = 30.dp, horizontal = 16.dp),
|
||||
)
|
||||
}
|
||||
|
||||
PrimaryButton(
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
.imePadding()
|
||||
.fillMaxWidth(),
|
||||
text = stringResourceSafe(id = R.string.common_continue),
|
||||
enabled = state.completeButtonEnabled,
|
||||
showProgress = state.completeButtonProgress,
|
||||
onClick = state.onCompleteButtonClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Fields(state: ManualBackupCheckUM, modifier: Modifier = Modifier) {
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
Column(
|
||||
modifier = modifier,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
DisableContextMenu {
|
||||
state.wordFields.fastForEachIndexed { index, field ->
|
||||
OutlineTextField(
|
||||
value = field.word,
|
||||
onValueChange = field.onChange,
|
||||
label = field.index.toString(),
|
||||
isError = field.error,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
autoCorrectEnabled = false,
|
||||
keyboardType = KeyboardType.Password,
|
||||
imeAction = if (index == state.wordFields.lastIndex) ImeAction.Done else ImeAction.Next,
|
||||
),
|
||||
keyboardActions = KeyboardActions(
|
||||
onDone = { focusManager.clearFocus() },
|
||||
onNext = { focusManager.moveFocus(focusDirection = FocusDirection.Down) },
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview() {
|
||||
TangemThemePreview {
|
||||
ManualBackupCheckContent(
|
||||
state = ManualBackupCheckUM(
|
||||
onCompleteButtonClick = {},
|
||||
wordFields = persistentListOf(
|
||||
ManualBackupCheckUM.WordField(
|
||||
index = 2,
|
||||
word = TextFieldValue(text = "word"),
|
||||
onChange = {},
|
||||
error = false,
|
||||
),
|
||||
ManualBackupCheckUM.WordField(
|
||||
index = 7,
|
||||
word = TextFieldValue(text = "wor"),
|
||||
onChange = {},
|
||||
error = true,
|
||||
),
|
||||
ManualBackupCheckUM.WordField(
|
||||
index = 11,
|
||||
word = TextFieldValue(text = "word"),
|
||||
onChange = {},
|
||||
error = false,
|
||||
),
|
||||
),
|
||||
completeButtonEnabled = false,
|
||||
completeButtonProgress = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package com.tangem.features.hotwallet.manualbackup.completed
|
||||
|
||||
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.decompose.ComposableContentComponent
|
||||
import com.tangem.features.hotwallet.manualbackup.completed.ui.ManualBackupCompletedContent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
internal class ManualBackupCompletedComponent @AssistedInject constructor(
|
||||
@Assisted private val context: AppComponentContext,
|
||||
@Assisted private val params: Params,
|
||||
) : ComposableContentComponent, AppComponentContext by context {
|
||||
private val model: ManualBackupCompletedModel = getOrCreateModel(params)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
ManualBackupCompletedContent(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
interface ModelCallbacks {
|
||||
fun onContinueClick()
|
||||
}
|
||||
|
||||
data class Params(
|
||||
val callbacks: ModelCallbacks,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package com.tangem.features.hotwallet.manualbackup.completed
|
||||
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.features.hotwallet.manualbackup.completed.entity.ManualBackupCompletedUM
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import javax.inject.Inject
|
||||
|
||||
@ModelScoped
|
||||
internal class ManualBackupCompletedModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
) : Model() {
|
||||
|
||||
private val params: ManualBackupCompletedComponent.Params = paramsContainer.require()
|
||||
|
||||
internal val uiState: StateFlow<ManualBackupCompletedUM>
|
||||
field = MutableStateFlow(
|
||||
ManualBackupCompletedUM(
|
||||
onContinueClick = params.callbacks::onContinueClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.tangem.features.hotwallet.manualbackup.completed.entity
|
||||
|
||||
internal data class ManualBackupCompletedUM(
|
||||
val onContinueClick: () -> Unit,
|
||||
)
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
package com.tangem.features.hotwallet.manualbackup.completed.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
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.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.features.hotwallet.manualbackup.completed.entity.ManualBackupCompletedUM
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
internal fun ManualBackupCompletedContent(state: ManualBackupCompletedUM, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Image(
|
||||
painter = painterResource(R.drawable.ic_success_blue_76),
|
||||
contentDescription = null,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
start = 48.dp,
|
||||
top = 20.dp,
|
||||
end = 48.dp,
|
||||
),
|
||||
text = stringResourceSafe(R.string.backup_complete_title),
|
||||
style = TangemTheme.typography.h2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
start = 48.dp,
|
||||
top = 12.dp,
|
||||
end = 48.dp,
|
||||
),
|
||||
text = stringResourceSafe(R.string.backup_complete_description),
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(2f))
|
||||
PrimaryButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = stringResourceSafe(R.string.common_continue),
|
||||
showProgress = false,
|
||||
enabled = true,
|
||||
onClick = state.onContinueClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun PreviewManualBackupCompletedContent() {
|
||||
TangemThemePreview {
|
||||
ManualBackupCompletedContent(
|
||||
state = ManualBackupCompletedUM(
|
||||
onContinueClick = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package com.tangem.features.hotwallet.manualbackup.phrase
|
||||
|
||||
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.decompose.ComposableContentComponent
|
||||
import com.tangem.features.hotwallet.manualbackup.phrase.model.ManualBackupPhraseModel
|
||||
import com.tangem.features.hotwallet.manualbackup.phrase.ui.ManualBackupPhraseContent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
internal class ManualBackupPhraseComponent @AssistedInject constructor(
|
||||
@Assisted private val context: AppComponentContext,
|
||||
@Assisted private val params: Params,
|
||||
) : ComposableContentComponent, AppComponentContext by context {
|
||||
private val model: ManualBackupPhraseModel = getOrCreateModel(params)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
ManualBackupPhraseContent(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
interface ModelCallbacks {
|
||||
fun onContinueClick()
|
||||
}
|
||||
|
||||
data class Params(
|
||||
val callbacks: ModelCallbacks,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.features.hotwallet.manualbackup.phrase.entity
|
||||
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
internal data class ManualBackupPhraseUM(
|
||||
val onContinueClick: () -> Unit,
|
||||
val words: ImmutableList<MnemonicGridItem> = persistentListOf(),
|
||||
) {
|
||||
data class MnemonicGridItem(
|
||||
val index: Int,
|
||||
val mnemonic: String,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package com.tangem.features.hotwallet.manualbackup.phrase.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.features.hotwallet.manualbackup.phrase.ManualBackupPhraseComponent
|
||||
import com.tangem.features.hotwallet.manualbackup.phrase.entity.ManualBackupPhraseUM
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import javax.inject.Inject
|
||||
|
||||
@Stable
|
||||
@ModelScoped
|
||||
internal class ManualBackupPhraseModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<ManualBackupPhraseComponent.Params>()
|
||||
private val callbacks = params.callbacks
|
||||
|
||||
internal val uiState: StateFlow<ManualBackupPhraseUM>
|
||||
field = MutableStateFlow(getInitialUIState())
|
||||
|
||||
private fun getInitialUIState(): ManualBackupPhraseUM {
|
||||
return ManualBackupPhraseUM(
|
||||
onContinueClick = callbacks::onContinueClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,181 @@
|
|||
package com.tangem.features.hotwallet.manualbackup.phrase.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.hotwallet.impl.R
|
||||
import com.tangem.features.hotwallet.manualbackup.phrase.entity.ManualBackupPhraseUM
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@Composable
|
||||
internal fun ManualBackupPhraseContent(state: ManualBackupPhraseUM, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.verticalScroll(rememberScrollState())
|
||||
.imePadding()
|
||||
.weight(1f),
|
||||
) {
|
||||
TitleBlock(
|
||||
state = state,
|
||||
modifier = Modifier.padding(top = 20.dp),
|
||||
)
|
||||
|
||||
SeedPhraseGridBlock(
|
||||
mnemonicGridItems = state.words,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 20.dp, bottom = 32.dp),
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.backup_seed_responsibility),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
)
|
||||
|
||||
PrimaryButton(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp),
|
||||
text = stringResourceSafe(id = R.string.common_continue),
|
||||
onClick = state.onContinueClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TitleBlock(state: ManualBackupPhraseUM, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.padding(horizontal = TangemTheme.dimens.size36)
|
||||
.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.backup_seed_title),
|
||||
style = TangemTheme.typography.h2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
Text(
|
||||
text = stringResourceSafe(
|
||||
R.string.backup_seed_description,
|
||||
state.words.size,
|
||||
),
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SeedPhraseGridBlock(
|
||||
mnemonicGridItems: ImmutableList<ManualBackupPhraseUM.MnemonicGridItem>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
VerticalGrid(
|
||||
modifier = modifier,
|
||||
items = mnemonicGridItems,
|
||||
) { item ->
|
||||
Row(
|
||||
modifier = Modifier.padding(all = TangemTheme.dimens.size8),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
if (LocalLayoutDirection.current == LayoutDirection.Ltr) {
|
||||
Text(
|
||||
modifier = Modifier.width(TangemTheme.dimens.size40),
|
||||
text = "${item.index}.",
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
)
|
||||
Text(
|
||||
text = item.mnemonic,
|
||||
style = TangemTheme.typography.button,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = item.mnemonic,
|
||||
style = TangemTheme.typography.button,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.width(TangemTheme.dimens.size40),
|
||||
text = "${item.index}.",
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private inline fun <T> VerticalGrid(
|
||||
items: ImmutableList<T>,
|
||||
modifier: Modifier = Modifier,
|
||||
crossinline content: @Composable (T) -> Unit,
|
||||
) {
|
||||
val columnLength = items.size / 2
|
||||
Row(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
) {
|
||||
repeat(2) { index ->
|
||||
Column {
|
||||
for (i in 0 until columnLength) {
|
||||
val item = items[index * columnLength + i]
|
||||
content(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360, heightDp = 640)
|
||||
@Preview(showBackground = true, widthDp = 360, heightDp = 640, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview() {
|
||||
TangemThemePreview {
|
||||
ManualBackupPhraseContent(
|
||||
state = ManualBackupPhraseUM(
|
||||
onContinueClick = {},
|
||||
words = List(12) {
|
||||
ManualBackupPhraseUM.MnemonicGridItem(
|
||||
index = it + 1,
|
||||
mnemonic = "word${it + 1}",
|
||||
)
|
||||
}.toImmutableList(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package com.tangem.features.hotwallet.manualbackup.start
|
||||
|
||||
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.decompose.ComposableContentComponent
|
||||
import com.tangem.features.hotwallet.manualbackup.start.ui.ManualBackupStartContent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
internal class ManualBackupStartComponent @AssistedInject constructor(
|
||||
@Assisted private val context: AppComponentContext,
|
||||
@Assisted private val params: Params,
|
||||
) : ComposableContentComponent, AppComponentContext by context {
|
||||
private val model: ManualBackupStartModel = getOrCreateModel(params)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
ManualBackupStartContent(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
interface ModelCallbacks {
|
||||
fun onContinueClick()
|
||||
}
|
||||
|
||||
data class Params(
|
||||
val callbacks: ModelCallbacks,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package com.tangem.features.hotwallet.manualbackup.start
|
||||
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.features.hotwallet.manualbackup.start.entity.ManualBackupStartUM
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import javax.inject.Inject
|
||||
|
||||
@ModelScoped
|
||||
internal class ManualBackupStartModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
) : Model() {
|
||||
|
||||
private val params: ManualBackupStartComponent.Params = paramsContainer.require()
|
||||
|
||||
internal val uiState: StateFlow<ManualBackupStartUM>
|
||||
field = MutableStateFlow(
|
||||
ManualBackupStartUM(
|
||||
onContinueClick = params.callbacks::onContinueClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.features.hotwallet.manualbackup.start.entity
|
||||
|
||||
internal data class ManualBackupStartUM(
|
||||
val seepPhraseLength: Int = 12,
|
||||
val onContinueClick: () -> Unit,
|
||||
)
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
package com.tangem.features.hotwallet.manualbackup.start.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
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.R
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.hotwallet.manualbackup.start.entity.ManualBackupStartUM
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
internal fun ManualBackupStartContent(state: ManualBackupStartUM, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.fillMaxSize()
|
||||
.padding(
|
||||
start = 16.dp,
|
||||
top = 24.dp,
|
||||
end = 16.dp,
|
||||
bottom = 16.dp,
|
||||
),
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
horizontal = 16.dp,
|
||||
vertical = 8.dp,
|
||||
),
|
||||
text = stringResourceSafe(R.string.backup_info_title),
|
||||
style = TangemTheme.typography.h2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
horizontal = 16.dp,
|
||||
vertical = 8.dp,
|
||||
),
|
||||
text = stringResourceSafe(
|
||||
R.string.backup_info_description,
|
||||
state.seepPhraseLength.toString(),
|
||||
),
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
FeatureBlock(
|
||||
modifier = Modifier
|
||||
.padding(top = 24.dp),
|
||||
title = stringResourceSafe(R.string.backup_info_save_title),
|
||||
description = stringResourceSafe(
|
||||
R.string.backup_info_save_description,
|
||||
state.seepPhraseLength.toString(),
|
||||
),
|
||||
iconRes = R.drawable.ic_lock_24,
|
||||
)
|
||||
FeatureBlock(
|
||||
modifier = Modifier
|
||||
.padding(top = 24.dp),
|
||||
title = stringResourceSafe(R.string.backup_info_keep_title),
|
||||
description = stringResourceSafe(R.string.backup_info_keep_description),
|
||||
iconRes = R.drawable.ic_settings_24,
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
PrimaryButton(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 16.dp),
|
||||
text = stringResourceSafe(R.string.common_continue),
|
||||
showProgress = false,
|
||||
enabled = true,
|
||||
onClick = state.onContinueClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FeatureBlock(title: String, description: String, iconRes: Int, modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 12.dp),
|
||||
painter = painterResource(iconRes),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.primary1,
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp),
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.padding(top = 4.dp),
|
||||
text = description,
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun PreviewManualBackupStartContent() {
|
||||
TangemThemePreview {
|
||||
ManualBackupStartContent(
|
||||
state = ManualBackupStartUM(
|
||||
onContinueClick = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
package com.tangem.features.hotwallet.setaccesscode
|
||||
|
||||
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.decompose.ComposableContentComponent
|
||||
import com.tangem.core.ui.security.DisableScreenshotsDisposableEffect
|
||||
import com.tangem.features.hotwallet.setaccesscode.ui.SetAccessCodeContent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
internal class SetAccessCodeComponent @AssistedInject constructor(
|
||||
@Assisted private val context: AppComponentContext,
|
||||
@Assisted private val params: Params,
|
||||
) : ComposableContentComponent, AppComponentContext by context {
|
||||
|
||||
private val model: SetAccessCodeModel = getOrCreateModel(params)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
SetAccessCodeContent(
|
||||
state = state,
|
||||
onBack = { model.onBack() },
|
||||
modifier = modifier,
|
||||
)
|
||||
|
||||
DisableScreenshotsDisposableEffect()
|
||||
}
|
||||
|
||||
interface ModelCallbacks {
|
||||
fun onBackClick()
|
||||
fun onAccessCodeSet()
|
||||
}
|
||||
|
||||
data class Params(
|
||||
val callbacks: ModelCallbacks,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
package com.tangem.features.hotwallet.setaccesscode
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.features.hotwallet.setaccesscode.entity.SetAccessCodeUM
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import javax.inject.Inject
|
||||
|
||||
@Stable
|
||||
@ModelScoped
|
||||
internal class SetAccessCodeModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<SetAccessCodeComponent.Params>()
|
||||
|
||||
internal val uiState: StateFlow<SetAccessCodeUM>
|
||||
field = MutableStateFlow(getInitialState())
|
||||
|
||||
fun onBack() {
|
||||
when (uiState.value.step) {
|
||||
SetAccessCodeUM.Step.AccessCode -> {
|
||||
params.callbacks.onBackClick()
|
||||
}
|
||||
SetAccessCodeUM.Step.ConfirmAccessCode -> {
|
||||
uiState.update {
|
||||
it.copy(
|
||||
step = SetAccessCodeUM.Step.AccessCode,
|
||||
accessCodeSecond = "",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getInitialState() = SetAccessCodeUM(
|
||||
step = SetAccessCodeUM.Step.AccessCode,
|
||||
accessCodeFirst = "",
|
||||
accessCodeSecond = "",
|
||||
onAccessCodeFirstChange = ::onAccessCodeFirstChange,
|
||||
onAccessCodeSecondChange = ::onAccessCodeSecondChange,
|
||||
buttonEnabled = false,
|
||||
onContinue = ::onContinue,
|
||||
)
|
||||
|
||||
private fun onAccessCodeFirstChange(value: String) {
|
||||
uiState.update {
|
||||
it.copy(
|
||||
accessCodeFirst = value,
|
||||
buttonEnabled = value.length == uiState.value.accessCodeLength,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onAccessCodeSecondChange(value: String) {
|
||||
uiState.update {
|
||||
it.copy(
|
||||
accessCodeSecond = value,
|
||||
buttonEnabled = uiState.value.accessCodeFirst == uiState.value.accessCodeSecond,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onContinue() {
|
||||
when (uiState.value.step) {
|
||||
SetAccessCodeUM.Step.AccessCode -> {
|
||||
uiState.update {
|
||||
it.copy(
|
||||
step = SetAccessCodeUM.Step.ConfirmAccessCode,
|
||||
)
|
||||
}
|
||||
}
|
||||
SetAccessCodeUM.Step.ConfirmAccessCode -> {
|
||||
params.callbacks.onAccessCodeSet()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.tangem.features.hotwallet.setaccesscode.entity
|
||||
|
||||
internal data class SetAccessCodeUM(
|
||||
val step: Step,
|
||||
val accessCodeFirst: String,
|
||||
val accessCodeSecond: String,
|
||||
val onAccessCodeFirstChange: (String) -> Unit,
|
||||
val onAccessCodeSecondChange: (String) -> Unit,
|
||||
val buttonEnabled: Boolean,
|
||||
val onContinue: () -> Unit,
|
||||
) {
|
||||
val accessCodeLength: Int = ACCESS_CODE_LENGTH
|
||||
|
||||
enum class Step {
|
||||
AccessCode,
|
||||
ConfirmAccessCode,
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ACCESS_CODE_LENGTH = 6
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
package com.tangem.features.hotwallet.setaccesscode.ui
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemAnimations
|
||||
import com.tangem.features.hotwallet.setaccesscode.entity.SetAccessCodeUM
|
||||
import com.tangem.features.hotwallet.setaccesscode.entity.SetAccessCodeUM.Step.*
|
||||
import com.tangem.core.res.R
|
||||
|
||||
@Composable
|
||||
internal fun SetAccessCodeContent(state: SetAccessCodeUM, onBack: () -> Unit, modifier: Modifier = Modifier) {
|
||||
BackHandler(onBack = onBack)
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.navigationBarsPadding(),
|
||||
) {
|
||||
AnimatedContent(
|
||||
modifier = Modifier.weight(1f),
|
||||
targetState = state.step,
|
||||
transitionSpec = TangemAnimations.AnimatedContent
|
||||
.slide { initial, target -> target.ordinal > initial.ordinal },
|
||||
label = "AnimatedContent",
|
||||
) { step ->
|
||||
when (step) {
|
||||
AccessCode -> SetAccessCodeEnter(
|
||||
modifier = Modifier.padding(top = 16.dp),
|
||||
state = state,
|
||||
reEnterAccessCodeState = false,
|
||||
)
|
||||
ConfirmAccessCode -> SetAccessCodeEnter(
|
||||
modifier = Modifier.padding(top = 16.dp),
|
||||
state = state,
|
||||
reEnterAccessCodeState = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
PrimaryButton(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
|
||||
.imePadding(),
|
||||
text = if (state.step == ConfirmAccessCode) {
|
||||
stringResourceSafe(R.string.common_confirm)
|
||||
} else {
|
||||
stringResourceSafe(R.string.common_continue)
|
||||
},
|
||||
onClick = state.onContinue,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
package com.tangem.features.hotwallet.setaccesscode.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
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.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.res.R
|
||||
import com.tangem.core.ui.components.fields.PinTextField
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.hotwallet.setaccesscode.entity.SetAccessCodeUM
|
||||
|
||||
@Composable
|
||||
internal fun SetAccessCodeEnter(
|
||||
state: SetAccessCodeUM,
|
||||
reEnterAccessCodeState: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.padding(horizontal = 16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.padding(top = 56.dp)
|
||||
.align(Alignment.CenterHorizontally),
|
||||
text = if (reEnterAccessCodeState) {
|
||||
stringResourceSafe(R.string.access_code_confirm_title)
|
||||
} else {
|
||||
stringResourceSafe(R.string.access_code_create_title)
|
||||
},
|
||||
style = TangemTheme.typography.h2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
.align(Alignment.CenterHorizontally),
|
||||
text = if (reEnterAccessCodeState) {
|
||||
stringResourceSafe(R.string.access_code_confirm_description)
|
||||
} else {
|
||||
stringResourceSafe(
|
||||
R.string.access_code_create_description,
|
||||
state.accessCodeLength,
|
||||
)
|
||||
},
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
PinTextField(
|
||||
length = state.accessCodeLength,
|
||||
isPasswordVisual = true,
|
||||
value = if (reEnterAccessCodeState) {
|
||||
state.accessCodeSecond
|
||||
} else {
|
||||
state.accessCodeFirst
|
||||
},
|
||||
onValueChange = if (reEnterAccessCodeState) {
|
||||
state.onAccessCodeSecondChange
|
||||
} else {
|
||||
state.onAccessCodeFirstChange
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview() {
|
||||
TangemThemePreview {
|
||||
SetAccessCodeEnter(
|
||||
reEnterAccessCodeState = false,
|
||||
state = SetAccessCodeUM(
|
||||
step = SetAccessCodeUM.Step.AccessCode,
|
||||
accessCodeFirst = "",
|
||||
accessCodeSecond = "",
|
||||
onAccessCodeFirstChange = {},
|
||||
onAccessCodeSecondChange = {},
|
||||
buttonEnabled = false,
|
||||
onContinue = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
private fun Preview2() {
|
||||
TangemThemePreview {
|
||||
SetAccessCodeEnter(
|
||||
reEnterAccessCodeState = true,
|
||||
state = SetAccessCodeUM(
|
||||
step = SetAccessCodeUM.Step.ConfirmAccessCode,
|
||||
accessCodeFirst = "",
|
||||
accessCodeSecond = "",
|
||||
onAccessCodeFirstChange = {},
|
||||
onAccessCodeSecondChange = {},
|
||||
buttonEnabled = false,
|
||||
onContinue = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package com.tangem.features.hotwallet.setupfinished
|
||||
|
||||
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.decompose.ComposableContentComponent
|
||||
import com.tangem.features.hotwallet.setupfinished.ui.MobileWalletSetupFinishedContent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
internal class MobileWalletSetupFinishedComponent @AssistedInject constructor(
|
||||
@Assisted private val context: AppComponentContext,
|
||||
@Assisted private val params: Params,
|
||||
) : ComposableContentComponent, AppComponentContext by context {
|
||||
private val model: MobileWalletSetupFinishedModel = getOrCreateModel(params)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
MobileWalletSetupFinishedContent(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
interface ModelCallbacks {
|
||||
fun onContinueClick()
|
||||
}
|
||||
|
||||
data class Params(
|
||||
val callbacks: ModelCallbacks,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package com.tangem.features.hotwallet.setupfinished
|
||||
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.features.hotwallet.setupfinished.entity.MobileWalletSetupFinishedUM
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import javax.inject.Inject
|
||||
|
||||
@ModelScoped
|
||||
internal class MobileWalletSetupFinishedModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
) : Model() {
|
||||
|
||||
private val params: MobileWalletSetupFinishedComponent.Params = paramsContainer.require()
|
||||
|
||||
internal val uiState: StateFlow<MobileWalletSetupFinishedUM>
|
||||
field = MutableStateFlow(
|
||||
MobileWalletSetupFinishedUM(
|
||||
onContinueClick = params.callbacks::onContinueClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.tangem.features.hotwallet.setupfinished.entity
|
||||
|
||||
internal data class MobileWalletSetupFinishedUM(
|
||||
val onContinueClick: () -> Unit,
|
||||
)
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
package com.tangem.features.hotwallet.setupfinished.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.airbnb.lottie.compose.LottieAnimation
|
||||
import com.airbnb.lottie.compose.LottieCompositionSpec
|
||||
import com.airbnb.lottie.compose.animateLottieCompositionAsState
|
||||
import com.airbnb.lottie.compose.rememberLottieComposition
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.FullScreen
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.features.hotwallet.setupfinished.entity.MobileWalletSetupFinishedUM
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
internal fun MobileWalletSetupFinishedContent(state: MobileWalletSetupFinishedUM, modifier: Modifier = Modifier) {
|
||||
val composition by rememberLottieComposition(spec = LottieCompositionSpec.RawRes(R.raw.anim_confetti))
|
||||
val progress by animateLottieCompositionAsState(composition)
|
||||
var showConfetti by remember { mutableStateOf(false) }
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Image(
|
||||
painter = painterResource(R.drawable.ic_success_blue_76),
|
||||
contentDescription = null,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
start = 48.dp,
|
||||
top = 20.dp,
|
||||
end = 48.dp,
|
||||
),
|
||||
text = stringResourceSafe(R.string.onboarding_done_header),
|
||||
style = TangemTheme.typography.h2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
start = 48.dp,
|
||||
top = 12.dp,
|
||||
end = 48.dp,
|
||||
),
|
||||
text = stringResourceSafe(R.string.backup_complete_description),
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(2f))
|
||||
PrimaryButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = stringResourceSafe(R.string.common_continue),
|
||||
showProgress = false,
|
||||
enabled = true,
|
||||
onClick = state.onContinueClick,
|
||||
)
|
||||
}
|
||||
|
||||
if (showConfetti) {
|
||||
FullScreen(notTouchable = true) {
|
||||
LottieAnimation(
|
||||
composition = composition,
|
||||
progress = { progress },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
showConfetti = true
|
||||
}
|
||||
|
||||
LaunchedEffect(progress == 1f) {
|
||||
if (progress == 1f) {
|
||||
showConfetti = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun PreviewMobileWalletSetupFinishedContent() {
|
||||
TangemThemePreview {
|
||||
MobileWalletSetupFinishedContent(
|
||||
state = MobileWalletSetupFinishedUM(
|
||||
onContinueClick = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ dependencies {
|
|||
/* Project - API */
|
||||
api(projects.features.markets.api)
|
||||
api(projects.features.onramp.api)
|
||||
api(projects.features.sendV2.api)
|
||||
|
||||
/* Data */
|
||||
implementation(projects.data.common)
|
||||
|
|
@ -74,7 +75,6 @@ dependencies {
|
|||
implementation(projects.core.analytics)
|
||||
implementation(projects.core.analytics.models)
|
||||
implementation(projects.core.navigation)
|
||||
implementation(projects.core.deepLinks)
|
||||
|
||||
/* Common */
|
||||
implementation(projects.common.ui)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ package com.tangem.features.markets.deeplink
|
|||
import arrow.core.getOrElse
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.core.deeplink.DeeplinkConst.TOKEN_ID_KEY
|
||||
import com.tangem.common.routing.deeplink.DeeplinkConst.TOKEN_ID_KEY
|
||||
import com.tangem.data.common.currency.getTokenIconUrlFromDefaultHost
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ import com.tangem.domain.models.currency.CryptoCurrency
|
|||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.domain.wallets.models.isMultiCurrency
|
||||
import com.tangem.domain.wallets.models.requireColdWallet
|
||||
import com.tangem.domain.wallets.usecase.GetCardImageUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
|
||||
import com.tangem.features.markets.impl.R
|
||||
|
|
@ -194,8 +193,7 @@ internal class MarketsPortfolioModel @Inject constructor(
|
|||
private fun loadArtworks(wallets: List<UserWallet>) {
|
||||
modelScope.launch {
|
||||
loadArtworksMutex.withLock {
|
||||
wallets.forEach { wallet ->
|
||||
wallet.requireColdWallet() // TODO [REDACTED_TASK_KEY]
|
||||
wallets.filterIsInstance<UserWallet.Cold>().forEach { wallet ->
|
||||
if (!loadedArtworks.containsKey(wallet.walletId)) {
|
||||
val artwork = getCardImageUseCase(
|
||||
cardId = wallet.cardId,
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import com.tangem.domain.wallets.models.UserWallet
|
|||
import com.tangem.features.markets.impl.R
|
||||
import com.tangem.features.markets.portfolio.impl.loader.PortfolioData
|
||||
import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContentUM
|
||||
import com.tangem.features.onramp.OnrampFeatureToggles
|
||||
import com.tangem.features.send.v2.api.SendFeatureToggles
|
||||
import com.tangem.utils.Provider
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
|
|
@ -41,14 +41,11 @@ internal class TokenActionsHandler @AssistedInject constructor(
|
|||
@Assisted private val onHandleQuickAction: (HandledQuickAction) -> Unit,
|
||||
private val isDemoCardUseCase: IsDemoCardUseCase,
|
||||
private val messageSender: UiMessageSender,
|
||||
private val onrampFeatureToggles: OnrampFeatureToggles,
|
||||
private val shareManager: ShareManager,
|
||||
private val sendFeatureToggles: SendFeatureToggles,
|
||||
) {
|
||||
|
||||
private val disabledActionsInDemoMode = buildSet {
|
||||
if (!onrampFeatureToggles.isFeatureEnabled) {
|
||||
add(TokenActionsBSContentUM.Action.Buy)
|
||||
}
|
||||
add(TokenActionsBSContentUM.Action.Sell)
|
||||
}
|
||||
|
||||
|
|
@ -132,12 +129,11 @@ internal class TokenActionsHandler @AssistedInject constructor(
|
|||
}
|
||||
|
||||
private fun onBuyClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) {
|
||||
reduxStateHolder.dispatch(
|
||||
TradeCryptoAction.Buy(
|
||||
userWallet = cryptoCurrencyData.userWallet,
|
||||
router.push(
|
||||
AppRoute.Onramp(
|
||||
userWalletId = cryptoCurrencyData.userWallet.walletId,
|
||||
currency = cryptoCurrencyData.status.currency,
|
||||
source = OnrampSource.MARKETS,
|
||||
cryptoCurrencyStatus = cryptoCurrencyData.status,
|
||||
appCurrencyCode = currentAppCurrency().code,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -163,12 +159,18 @@ internal class TokenActionsHandler @AssistedInject constructor(
|
|||
}
|
||||
|
||||
private fun onSendClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) {
|
||||
router.push(
|
||||
val route = if (sendFeatureToggles.isSendWithSwapEnabled) {
|
||||
AppRoute.SendEntryPoint(
|
||||
userWalletId = cryptoCurrencyData.userWallet.walletId,
|
||||
currency = cryptoCurrencyData.status.currency,
|
||||
)
|
||||
} else {
|
||||
AppRoute.Send(
|
||||
userWalletId = cryptoCurrencyData.userWallet.walletId,
|
||||
currency = cryptoCurrencyData.status.currency,
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
router.push(route)
|
||||
}
|
||||
|
||||
private fun onStakeClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
package com.tangem.features.nft
|
||||
|
||||
interface NFTFeatureToggles {
|
||||
val isNFTEnabled: Boolean
|
||||
val isNFTEVMEnabled: Boolean
|
||||
val isNFTSolanaEnabled: Boolean
|
||||
|
||||
val isNFTMediaContentEnabled: Boolean
|
||||
}
|
||||
|
|
@ -5,14 +5,6 @@ import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
|||
internal class DefaultNFTFeatureToggles(
|
||||
private val featureTogglesManager: FeatureTogglesManager,
|
||||
) : NFTFeatureToggles {
|
||||
override val isNFTEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled(name = "NFT_ENABLED")
|
||||
|
||||
override val isNFTEVMEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled(name = "NFT_EVM_ENABLED")
|
||||
|
||||
override val isNFTSolanaEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled(name = "NFT_SOLANA_ENABLED")
|
||||
|
||||
override val isNFTMediaContentEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled(name = "NFT_MEDIA_CONTENT_ENABLED")
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ internal fun NFTContent(stackState: ChildStack<NFTRoute, ComposableContentCompon
|
|||
.fillMaxSize()
|
||||
.imePadding()
|
||||
.systemBarsPadding(),
|
||||
horizontalAlignment = Alignment.Companion.CenterHorizontally,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Children(
|
||||
stack = stackState,
|
||||
|
|
|
|||
|
|
@ -71,13 +71,11 @@ internal fun MultiWalletAccessCodeEnter(
|
|||
state.onAccessCodeFirstChange
|
||||
},
|
||||
label = stringResourceSafe(id = R.string.onboarding_wallet_info_title_third),
|
||||
isError = state.codesNotMatchError || state.atLeast4CharError,
|
||||
isError = state.codesNotMatchError,
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
caption = when {
|
||||
state.codesNotMatchError && reEnterAccessCodeState ->
|
||||
stringResourceSafe(R.string.onboarding_access_codes_doesnt_match)
|
||||
state.atLeast4CharError && !reEnterAccessCodeState ->
|
||||
stringResourceSafe(R.string.onboarding_access_code_too_short)
|
||||
else -> null
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -19,9 +19,11 @@ import com.tangem.domain.models.scan.ScanResponse
|
|||
import com.tangem.domain.onramp.GetLegacyTopUpUrlUseCase
|
||||
import com.tangem.domain.tokens.FetchCurrencyStatusUseCase
|
||||
import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase
|
||||
import com.tangem.domain.tokens.TokensFeatureToggles
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
|
||||
import com.tangem.domain.tokens.model.analytics.TokenReceiveAnalyticsEvent
|
||||
import com.tangem.domain.tokens.wallet.WalletBalanceFetcher
|
||||
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
|
||||
|
|
@ -32,6 +34,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
|||
import com.tangem.utils.extensions.isPositive
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
|
|
@ -49,6 +52,8 @@ internal class OnboardingNoteTopUpModel @Inject constructor(
|
|||
private val rampStateManager: RampStateManager,
|
||||
private val cardRepository: CardRepository,
|
||||
private val saveWalletUseCase: SaveWalletUseCase,
|
||||
private val walletBalanceFetcher: WalletBalanceFetcher,
|
||||
private val tokensFeatureToggles: TokensFeatureToggles,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<OnboardingNoteTopUpComponent.Params>()
|
||||
|
|
@ -83,10 +88,12 @@ internal class OnboardingNoteTopUpModel @Inject constructor(
|
|||
showBalanceLoadingProgress(true)
|
||||
createUserWalletIfNull()
|
||||
val userWalletId = requireNotNull(userWallet?.walletId)
|
||||
fetchCurrencyStatusUseCase(
|
||||
userWalletId = userWalletId,
|
||||
refresh = true,
|
||||
)
|
||||
if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) {
|
||||
walletBalanceFetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId))
|
||||
.onLeft(Timber::e)
|
||||
} else {
|
||||
fetchCurrencyStatusUseCase(userWalletId = userWalletId, refresh = true)
|
||||
}
|
||||
showBalanceLoadingProgress(false)
|
||||
}
|
||||
}
|
||||
|
|
@ -197,12 +204,9 @@ internal class OnboardingNoteTopUpModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun loadAvailableForBuy(cryptoCurrencyStatus: CryptoCurrencyStatus) {
|
||||
val userWalletId = userWallet?.walletId ?: return
|
||||
|
||||
modelScope.launch {
|
||||
val availableForBuy = rampStateManager.availableForBuy(
|
||||
scanResponse = scanResponse,
|
||||
userWalletId = userWalletId,
|
||||
userWallet = userWallet ?: return@launch,
|
||||
cryptoCurrency = cryptoCurrencyStatus.currency,
|
||||
)
|
||||
_uiState.update {
|
||||
|
|
|
|||
|
|
@ -36,8 +36,10 @@ import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase
|
|||
import com.tangem.domain.onramp.GetLegacyTopUpUrlUseCase
|
||||
import com.tangem.domain.tokens.FetchCurrencyStatusUseCase
|
||||
import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase
|
||||
import com.tangem.domain.tokens.TokensFeatureToggles
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.model.analytics.TokenReceiveAnalyticsEvent
|
||||
import com.tangem.domain.tokens.wallet.WalletBalanceFetcher
|
||||
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
|
||||
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
|
|
@ -85,6 +87,8 @@ internal class OnboardingTwinModel @Inject constructor(
|
|||
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
|
||||
private val clipboardManager: ClipboardManager,
|
||||
private val shareManager: ShareManager,
|
||||
private val tokensFeatureToggles: TokensFeatureToggles,
|
||||
private val walletBalanceFetcher: WalletBalanceFetcher,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<OnboardingTwinComponent.Params>()
|
||||
|
|
@ -329,13 +333,15 @@ internal class OnboardingTwinModel @Inject constructor(
|
|||
|
||||
cardRepository.finishCardActivation(params.scanResponse.card.cardId)
|
||||
|
||||
fetchCurrencyStatusUseCase.invoke(
|
||||
userWalletId = userWallet.walletId,
|
||||
refresh = true,
|
||||
).onLeft {
|
||||
Timber.e("Unable to fetch currency status: $it")
|
||||
setLoading(false)
|
||||
if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) {
|
||||
walletBalanceFetcher(params = WalletBalanceFetcher.Params(userWalletId = userWallet.walletId))
|
||||
} else {
|
||||
fetchCurrencyStatusUseCase.invoke(userWalletId = userWallet.walletId, refresh = true)
|
||||
}
|
||||
.onLeft {
|
||||
Timber.e("Unable to fetch currency status: $it")
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
val cryptoCurrencyStatus = getSingleCryptoCurrencyStatusUseCase.invokeSingleWallet(userWallet.walletId)
|
||||
.firstOrNull()?.getOrNull()
|
||||
|
|
@ -431,10 +437,12 @@ internal class OnboardingTwinModel @Inject constructor(
|
|||
it.copy(isLoading = true)
|
||||
}
|
||||
modelScope.launch {
|
||||
fetchCurrencyStatusUseCase(
|
||||
userWalletId = userWallet.walletId,
|
||||
refresh = true,
|
||||
)
|
||||
if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) {
|
||||
walletBalanceFetcher(params = WalletBalanceFetcher.Params(userWalletId = userWallet.walletId))
|
||||
.onLeft(Timber::e)
|
||||
} else {
|
||||
fetchCurrencyStatusUseCase(userWalletId = userWallet.walletId, refresh = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ dependencies {
|
|||
/* Project - Core */
|
||||
implementation(projects.core.decompose)
|
||||
implementation(projects.core.ui)
|
||||
implementation(projects.core.deepLinks)
|
||||
|
||||
/* Project - Domain */
|
||||
implementation(projects.domain.onramp.models)
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
package com.tangem.features.onramp
|
||||
|
||||
interface OnrampFeatureToggles {
|
||||
|
||||
val isFeatureEnabled: Boolean
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
package com.tangem.features.onramp.deeplink
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
interface BuyRedirectDeepLinkHandler {
|
||||
|
||||
interface Factory {
|
||||
fun create(coroutineScope: CoroutineScope): BuyRedirectDeepLinkHandler
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
package com.tangem.features.onramp.deeplink
|
||||
|
||||
import com.tangem.core.deeplink.DeepLink
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
@Deprecated("Use OnrampDeepLinkHandler")
|
||||
abstract class OnrampDeepLink : DeepLink() {
|
||||
override val uri = "tangem://onramp"
|
||||
|
||||
interface Factory {
|
||||
fun create(coroutineScope: CoroutineScope): OnrampDeepLink
|
||||
}
|
||||
}
|
||||
|
||||
interface OnrampDeepLinkHandler {
|
||||
|
||||
interface Factory {
|
||||
fun create(coroutineScope: CoroutineScope, queryParams: Map<String, String>): OnrampDeepLinkHandler
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.features.onramp.deeplink
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
interface OnrampDeepLinkHandler {
|
||||
|
||||
interface Factory {
|
||||
fun create(coroutineScope: CoroutineScope, queryParams: Map<String, String>): OnrampDeepLinkHandler
|
||||
}
|
||||
}
|
||||
|
|
@ -22,7 +22,6 @@ dependencies {
|
|||
implementation(projects.core.analytics)
|
||||
implementation(projects.core.configToggles)
|
||||
implementation(projects.core.decompose)
|
||||
implementation(projects.core.deepLinks)
|
||||
implementation(projects.core.navigation)
|
||||
implementation(projects.core.ui)
|
||||
implementation(projects.core.utils)
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
package com.tangem.features.onramp
|
||||
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
|
||||
internal class DefaultOnrampFeatureToggles(
|
||||
private val featureTogglesManager: FeatureTogglesManager,
|
||||
) : OnrampFeatureToggles {
|
||||
|
||||
override val isFeatureEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled(name = "ONRAMP_ENABLED")
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
package com.tangem.features.onramp
|
||||
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object OnrampFeatureModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideFeatureToggles(featureTogglesManager: FeatureTogglesManager): OnrampFeatureToggles {
|
||||
return DefaultOnrampFeatureToggles(featureTogglesManager)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
package com.tangem.features.onramp.deeplink
|
||||
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.domain.tokens.GetCryptoCurrencyUseCase
|
||||
import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent
|
||||
import com.tangem.domain.wallets.models.isMultiCurrency
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||
import com.tangem.features.onramp.OnrampFeatureToggles
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
internal class DefaultBuyRedirectDeepLinkHandler @AssistedInject constructor(
|
||||
@Assisted scope: CoroutineScope,
|
||||
onrampFeatureToggles: OnrampFeatureToggles,
|
||||
getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
|
||||
getCryptoCurrencyUseCase: GetCryptoCurrencyUseCase,
|
||||
analyticsEventHandler: AnalyticsEventHandler,
|
||||
) : BuyRedirectDeepLinkHandler {
|
||||
|
||||
init {
|
||||
// It is okay here, we are navigating from outside, and there is no other way to getting UserWallet
|
||||
getSelectedWalletSyncUseCase().fold(
|
||||
ifLeft = {
|
||||
Timber.e("Error on getting user wallet: $it")
|
||||
},
|
||||
ifRight = { userWallet ->
|
||||
if (!onrampFeatureToggles.isFeatureEnabled && !userWallet.isMultiCurrency) {
|
||||
scope.launch {
|
||||
val cryptoCurrency = getCryptoCurrencyUseCase(userWallet.walletId).getOrElse {
|
||||
Timber.e("Error on getting cryptoCurrency: $it")
|
||||
return@launch
|
||||
}
|
||||
analyticsEventHandler.send(TokenScreenAnalyticsEvent.Bought(cryptoCurrency.symbol))
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : BuyRedirectDeepLinkHandler.Factory {
|
||||
override fun create(coroutineScope: CoroutineScope): DefaultBuyRedirectDeepLinkHandler
|
||||
}
|
||||
}
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
package com.tangem.features.onramp.deeplink
|
||||
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.features.onramp.success.OnrampSuccessScreenTrigger
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
internal class DefaultOnrampDeepLink @AssistedInject constructor(
|
||||
private val appRouter: AppRouter,
|
||||
private val onrampSuccessScreenTrigger: OnrampSuccessScreenTrigger,
|
||||
@Assisted private val scope: CoroutineScope,
|
||||
) : OnrampDeepLink() {
|
||||
|
||||
override fun onReceive(params: Map<String, String>) {
|
||||
val txId = params[TX_ID_KEY]
|
||||
val result = OnrampRedirectResult.getResult(params[RESULT_KEY])
|
||||
|
||||
when {
|
||||
!txId.isNullOrEmpty() -> {
|
||||
// finish current onramp flow and show onramp success screen
|
||||
val replaceOnrampScreens = appRouter.stack
|
||||
.filterNot { it is AppRoute.Onramp }
|
||||
.toMutableList()
|
||||
replaceOnrampScreens.add(AppRoute.OnrampSuccess(txId))
|
||||
appRouter.replaceAll(*replaceOnrampScreens.toTypedArray())
|
||||
}
|
||||
result != OnrampRedirectResult.Unknown -> {
|
||||
scope.launch {
|
||||
onrampSuccessScreenTrigger.triggerOnrampSuccess(result == OnrampRedirectResult.Success)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : OnrampDeepLink.Factory {
|
||||
override fun create(coroutineScope: CoroutineScope): DefaultOnrampDeepLink
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val TX_ID_KEY = "tx_id"
|
||||
const val RESULT_KEY = "result"
|
||||
}
|
||||
}
|
||||
|
|
@ -11,20 +11,10 @@ import javax.inject.Singleton
|
|||
@InstallIn(SingletonComponent::class)
|
||||
internal interface OnrampDeeplinkModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindFactory(impl: DefaultOnrampDeepLink.Factory): OnrampDeepLink.Factory
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindOnrampDeepLinkHandlerFactory(impl: DefaultOnrampDeepLinkHandler.Factory): OnrampDeepLinkHandler.Factory
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindBuyRedirectDeepLinkHandler(
|
||||
impl: DefaultBuyRedirectDeepLinkHandler.Factory,
|
||||
): BuyRedirectDeepLinkHandler.Factory
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindBuyDeepLinkHandler(impl: DefaultBuyDeepLinkHandler.Factory): BuyDeepLinkHandler.Factory
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.features.onramp.selecttoken.model
|
||||
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.common.ui.alerts.models.AlertDemoModeUM
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
|
|
@ -23,7 +24,6 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
|||
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
|
||||
import com.tangem.domain.wallets.models.requireColdWallet
|
||||
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||
import com.tangem.features.onramp.OnrampFeatureToggles
|
||||
import com.tangem.features.onramp.impl.R
|
||||
import com.tangem.features.onramp.selecttoken.OnrampOperationComponent.Params
|
||||
import com.tangem.features.onramp.selecttoken.entity.OnrampOperationUM
|
||||
|
|
@ -46,7 +46,6 @@ internal class OnrampOperationModel @Inject constructor(
|
|||
private val reduxStateHolder: ReduxStateHolder,
|
||||
private val isDemoCardUseCase: IsDemoCardUseCase,
|
||||
private val messageSender: UiMessageSender,
|
||||
private val onrampFeatureToggles: OnrampFeatureToggles,
|
||||
private val rampStateManager: RampStateManager,
|
||||
) : Model() {
|
||||
|
||||
|
|
@ -84,8 +83,7 @@ internal class OnrampOperationModel @Inject constructor(
|
|||
fun onHotTokenClick(status: CryptoCurrencyStatus) {
|
||||
modelScope.launch {
|
||||
val unavailabilityReason = rampStateManager.availableForBuy(
|
||||
scanResponse = selectedUserWallet.scanResponse,
|
||||
userWalletId = params.userWalletId,
|
||||
userWallet = selectedUserWallet,
|
||||
cryptoCurrency = status.currency,
|
||||
)
|
||||
|
||||
|
|
@ -100,7 +98,7 @@ internal class OnrampOperationModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun selectTokenIfDemoModeOff(status: CryptoCurrencyStatus) {
|
||||
if (params is Params.Sell || !onrampFeatureToggles.isFeatureEnabled) {
|
||||
if (params is Params.Sell) {
|
||||
showErrorIfDemoModeOrElse { selectToken(status) }
|
||||
} else {
|
||||
selectToken(status)
|
||||
|
|
@ -109,14 +107,25 @@ internal class OnrampOperationModel @Inject constructor(
|
|||
|
||||
private fun selectToken(status: CryptoCurrencyStatus) {
|
||||
modelScope.launch {
|
||||
val appCurrencyCode = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default }.code
|
||||
when (params) {
|
||||
is Params.Buy -> {
|
||||
router.push(
|
||||
AppRoute.Onramp(
|
||||
userWalletId = selectedUserWallet.walletId,
|
||||
currency = status.currency,
|
||||
source = OnrampSource.ACTION_BUTTONS,
|
||||
),
|
||||
)
|
||||
}
|
||||
is Params.Sell -> {
|
||||
val appCurrencyCode = getSelectedAppCurrencyUseCase.invokeSync()
|
||||
.getOrElse { AppCurrency.Default }.code
|
||||
|
||||
reduxStateHolder.dispatch(
|
||||
action = when (params) {
|
||||
is Params.Buy -> getBuyAction(status, appCurrencyCode)
|
||||
is Params.Sell -> TradeCryptoAction.Sell(status, appCurrencyCode)
|
||||
},
|
||||
)
|
||||
reduxStateHolder.dispatch(
|
||||
action = TradeCryptoAction.Sell(status, appCurrencyCode),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -144,15 +153,6 @@ internal class OnrampOperationModel @Inject constructor(
|
|||
router.pop()
|
||||
}
|
||||
|
||||
private fun getBuyAction(status: CryptoCurrencyStatus, appCurrencyCode: String): TradeCryptoAction {
|
||||
return TradeCryptoAction.Buy(
|
||||
userWallet = selectedUserWallet,
|
||||
cryptoCurrencyStatus = status,
|
||||
source = OnrampSource.ACTION_BUTTONS,
|
||||
appCurrencyCode = appCurrencyCode,
|
||||
)
|
||||
}
|
||||
|
||||
private fun showErrorIfDemoModeOrElse(action: () -> Unit) {
|
||||
if (isDemoCardUseCase(cardId = selectedUserWallet.cardId)) {
|
||||
val alertUM = AlertDemoModeUM(onConfirmClick = {})
|
||||
|
|
|
|||
|
|
@ -229,8 +229,7 @@ internal class OnrampTokenListModel @Inject constructor(
|
|||
return when (params.filterOperation) {
|
||||
OnrampOperation.BUY -> {
|
||||
rampStateManager.availableForBuy(
|
||||
scanResponse = userWallet.scanResponse,
|
||||
userWalletId = params.userWalletId,
|
||||
userWallet = userWallet,
|
||||
cryptoCurrency = status.currency,
|
||||
).isAvailable()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,4 +16,7 @@ dependencies {
|
|||
implementation(projects.core.decompose)
|
||||
implementation(projects.core.ui)
|
||||
implementation(projects.core.analytics.models)
|
||||
|
||||
/** Common */
|
||||
implementation(projects.common.routing)
|
||||
}
|
||||
|
|
@ -1,9 +1,19 @@
|
|||
package com.tangem.features.pushnotifications.api
|
||||
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
|
||||
interface PushNotificationsComponent : ComposableContentComponent {
|
||||
|
||||
interface Factory : ComponentFactory<Unit, PushNotificationsComponent>
|
||||
interface Factory : ComponentFactory<Params, PushNotificationsComponent>
|
||||
|
||||
interface ModelCallbacks {
|
||||
fun onResult()
|
||||
}
|
||||
|
||||
sealed class Params {
|
||||
data class Callbacks(val callbacks: ModelCallbacks) : Params()
|
||||
data class Route(val route: AppRoute) : Params()
|
||||
}
|
||||
}
|
||||
|
|
@ -19,7 +19,7 @@ import dagger.assisted.AssistedInject
|
|||
|
||||
internal class DefaultPushNotificationsComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted params: Unit,
|
||||
@Assisted params: PushNotificationsComponent.Params,
|
||||
) : PushNotificationsComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: PushNotificationsModel = getOrCreateModel(params)
|
||||
|
|
@ -41,6 +41,9 @@ internal class DefaultPushNotificationsComponent @AssistedInject constructor(
|
|||
|
||||
@AssistedFactory
|
||||
interface Factory : PushNotificationsComponent.Factory {
|
||||
override fun create(context: AppComponentContext, params: Unit): DefaultPushNotificationsComponent
|
||||
override fun create(
|
||||
context: AppComponentContext,
|
||||
params: PushNotificationsComponent.Params,
|
||||
): DefaultPushNotificationsComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +1,16 @@
|
|||
package com.tangem.features.pushnotifications.impl.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
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.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.domain.notifications.toggles.NotificationsFeatureToggles
|
||||
import com.tangem.domain.settings.NeverRequestPermissionUseCase
|
||||
import com.tangem.domain.settings.NeverToInitiallyAskPermissionUseCase
|
||||
import com.tangem.features.pushnotifications.api.PushNotificationsComponent
|
||||
import com.tangem.features.pushnotifications.api.analytics.PushNotificationAnalyticEvents
|
||||
import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION
|
||||
import com.tangem.features.pushnotifications.impl.presentation.state.PushNotificationsUM
|
||||
|
|
@ -21,15 +22,19 @@ import javax.inject.Inject
|
|||
|
||||
@Stable
|
||||
@ModelScoped
|
||||
@Suppress("LongParameterList")
|
||||
internal class PushNotificationsModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val neverRequestPermissionUseCase: NeverRequestPermissionUseCase,
|
||||
private val neverToInitiallyAskPermissionUseCase: NeverToInitiallyAskPermissionUseCase,
|
||||
private val appRouter: AppRouter,
|
||||
private val analyticHandler: AnalyticsEventHandler,
|
||||
private val notificationsFeatureToggles: NotificationsFeatureToggles,
|
||||
notificationsFeatureToggles: NotificationsFeatureToggles,
|
||||
) : Model(), PushNotificationsClickIntents {
|
||||
|
||||
private val params: PushNotificationsComponent.Params = paramsContainer.require()
|
||||
|
||||
private val _state = MutableStateFlow(
|
||||
PushNotificationsUM(
|
||||
showInfoAboutNotifications = notificationsFeatureToggles.isNotificationsEnabled,
|
||||
|
|
@ -51,7 +56,7 @@ internal class PushNotificationsModel @Inject constructor(
|
|||
modelScope.launch {
|
||||
neverRequestPermissionUseCase(PUSH_PERMISSION)
|
||||
neverToInitiallyAskPermissionUseCase(PUSH_PERMISSION)
|
||||
appRouter.push(AppRoute.Home)
|
||||
onResult()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -62,7 +67,7 @@ internal class PushNotificationsModel @Inject constructor(
|
|||
modelScope.launch {
|
||||
neverRequestPermissionUseCase(PUSH_PERMISSION)
|
||||
neverToInitiallyAskPermissionUseCase(PUSH_PERMISSION)
|
||||
appRouter.push(AppRoute.Home)
|
||||
onResult()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -73,7 +78,18 @@ internal class PushNotificationsModel @Inject constructor(
|
|||
modelScope.launch {
|
||||
neverRequestPermissionUseCase(PUSH_PERMISSION)
|
||||
neverToInitiallyAskPermissionUseCase(PUSH_PERMISSION)
|
||||
appRouter.push(AppRoute.Home)
|
||||
onResult()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onResult() {
|
||||
when (params) {
|
||||
is PushNotificationsComponent.Params.Callbacks -> {
|
||||
params.callbacks.onResult()
|
||||
}
|
||||
is PushNotificationsComponent.Params.Route -> {
|
||||
appRouter.push(params.route)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -18,6 +18,7 @@ import com.google.mlkit.vision.common.InputImage
|
|||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.components.SystemBarsIconsDisposable
|
||||
import com.tangem.domain.qrscanning.models.QrResultSource
|
||||
import com.tangem.feature.qrscanning.inner.MLKitBarcodeAnalyzer
|
||||
import com.tangem.feature.qrscanning.model.QrScanningModel
|
||||
import com.tangem.feature.qrscanning.presentation.QrScanningContent
|
||||
|
|
@ -41,10 +42,10 @@ class DefaultQrScanningComponent @AssistedInject constructor(
|
|||
// Camera requires its own analyzer instance due to flow of frames needed to be analyzed.
|
||||
// Each new frame can cancel previous analysis e.i. image from the gallery can be skipped.
|
||||
private val cameraAnalyzer: MLKitBarcodeAnalyzer by lazy(LazyThreadSafetyMode.NONE) {
|
||||
MLKitBarcodeAnalyzer(model::onQrScanned)
|
||||
MLKitBarcodeAnalyzer { qrCode -> model.onQrScanned(qrCode, QrResultSource.CAMERA) }
|
||||
}
|
||||
private val analyzer: MLKitBarcodeAnalyzer by lazy(LazyThreadSafetyMode.NONE) {
|
||||
MLKitBarcodeAnalyzer(model::onQrScanned)
|
||||
MLKitBarcodeAnalyzer { qrCode -> model.onQrScanned(qrCode, QrResultSource.GALLERY) }
|
||||
}
|
||||
|
||||
init {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.feature.qrscanning.model
|
||||
|
||||
import com.tangem.domain.qrscanning.models.QrResultSource
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
|
||||
internal interface QrScanningClickIntents {
|
||||
|
|
@ -8,7 +9,7 @@ internal interface QrScanningClickIntents {
|
|||
|
||||
fun onBackClick()
|
||||
|
||||
fun onQrScanned(qrCode: String)
|
||||
fun onQrScanned(qrCode: String, source: QrResultSource)
|
||||
|
||||
fun onGalleryClicked()
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import com.tangem.core.decompose.model.ParamsContainer
|
|||
import com.tangem.core.navigation.settings.SettingsManager
|
||||
import com.tangem.core.ui.clipboard.ClipboardManager
|
||||
import com.tangem.data.card.sdk.CardSdkProvider
|
||||
import com.tangem.domain.qrscanning.models.QrResultSource
|
||||
import com.tangem.domain.qrscanning.models.RawQrResult
|
||||
import com.tangem.domain.qrscanning.usecases.EmitQrScannedEventUseCase
|
||||
import com.tangem.feature.qrscanning.QrScanningComponent
|
||||
import com.tangem.feature.qrscanning.presentation.QrScanningState
|
||||
|
|
@ -71,10 +73,11 @@ internal class QrScanningModel @Inject constructor(
|
|||
|
||||
override fun onBackClick() = appRouter.pop()
|
||||
|
||||
override fun onQrScanned(qrCode: String) {
|
||||
override fun onQrScanned(qrCode: String, source: QrResultSource) {
|
||||
if (qrCode.isNotBlank()) {
|
||||
modelScope.launch(dispatchers.mainImmediate) {
|
||||
emitQrScannedEventUseCase.invoke(params.source, qrCode)
|
||||
val qrCode = RawQrResult(qrCode, source, params.source)
|
||||
emitQrScannedEventUseCase.invoke(qrCode)
|
||||
}
|
||||
if (!isScanned) {
|
||||
appRouter.pop()
|
||||
|
|
|
|||
|
|
@ -65,8 +65,12 @@ internal fun QrScanningContent(
|
|||
|
||||
TangemTopAppBar(
|
||||
modifier = Modifier.statusBarsPadding(),
|
||||
title = null,
|
||||
startButton = TopAppBarButtonUM.Back(uiState.onBackClick),
|
||||
title = uiState.topBarConfig.title?.resolveReference(),
|
||||
startButton = TopAppBarButtonUM(
|
||||
iconRes = uiState.topBarConfig.startIcon,
|
||||
onIconClicked = uiState.onBackClick,
|
||||
),
|
||||
textColor = TangemTheme.colors.text.constantWhite,
|
||||
iconTint = TangemColorPalette.White,
|
||||
containerColor = Color.Transparent,
|
||||
endContent = {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
package com.tangem.feature.qrscanning.presentation
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
@Immutable
|
||||
internal data class QrScanningState(
|
||||
val topBarConfig: TopBarConfig,
|
||||
val message: TextReference?,
|
||||
val onQrScanned: (String) -> Unit,
|
||||
val onBackClick: () -> Unit,
|
||||
|
|
@ -14,6 +16,8 @@ internal data class QrScanningState(
|
|||
val bottomSheetConfig: TangemBottomSheetConfig? = null,
|
||||
)
|
||||
|
||||
internal data class TopBarConfig(val title: TextReference?, @DrawableRes val startIcon: Int)
|
||||
|
||||
@Immutable
|
||||
internal sealed interface PasteAction {
|
||||
data object None : PasteAction
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ internal class QrScanningStateController @Inject constructor() {
|
|||
|
||||
private fun getInitialState(): QrScanningState {
|
||||
return QrScanningState(
|
||||
topBarConfig = TopBarConfig(title = null, startIcon = 0),
|
||||
message = null,
|
||||
onBackClick = {},
|
||||
onQrScanned = {},
|
||||
|
|
|
|||
|
|
@ -4,13 +4,13 @@ import com.tangem.core.ui.clipboard.ClipboardManager
|
|||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.domain.qrscanning.models.QrResultSource
|
||||
import com.tangem.domain.qrscanning.models.SourceType
|
||||
import com.tangem.feature.qrscanning.impl.R
|
||||
import com.tangem.feature.qrscanning.model.QrScanningClickIntents
|
||||
import com.tangem.feature.qrscanning.presentation.PasteAction
|
||||
import com.tangem.feature.qrscanning.presentation.QrScanningState
|
||||
|
||||
private const val WC_SCHEME = "wc"
|
||||
import com.tangem.feature.qrscanning.presentation.TopBarConfig
|
||||
|
||||
internal class InitializeQrScanningStateTransformer(
|
||||
private val clickIntents: QrScanningClickIntents,
|
||||
|
|
@ -26,24 +26,34 @@ internal class InitializeQrScanningStateTransformer(
|
|||
}
|
||||
|
||||
return QrScanningState(
|
||||
topBarConfig = constructTopBarConfig(),
|
||||
message = message,
|
||||
onBackClick = clickIntents::onBackClick,
|
||||
onQrScanned = clickIntents::onQrScanned,
|
||||
onQrScanned = { qrCode -> clickIntents.onQrScanned(qrCode, QrResultSource.CAMERA) },
|
||||
onGalleryClick = clickIntents::onGalleryClicked,
|
||||
pasteAction = constructPasteAction(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun constructTopBarConfig(): TopBarConfig {
|
||||
return when (source) {
|
||||
SourceType.SEND -> TopBarConfig(
|
||||
title = resourceReference(R.string.common_send),
|
||||
startIcon = R.drawable.ic_back_24,
|
||||
)
|
||||
SourceType.WALLET_CONNECT -> TopBarConfig(
|
||||
title = resourceReference(R.string.wc_new_connection),
|
||||
startIcon = R.drawable.ic_close_24,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun constructPasteAction(): PasteAction {
|
||||
val uri = clipboardManager.getText()
|
||||
return if (source == SourceType.WALLET_CONNECT && uri != null && isWalletConnectUri(uri)) {
|
||||
PasteAction.Perform { clickIntents.onQrScanned(uri) }
|
||||
return if (uri != null) {
|
||||
PasteAction.Perform { clickIntents.onQrScanned(uri, QrResultSource.CLIPBOARD) }
|
||||
} else {
|
||||
PasteAction.None
|
||||
}
|
||||
}
|
||||
|
||||
private fun isWalletConnectUri(uri: String): Boolean {
|
||||
return uri.lowercase().startsWith(WC_SCHEME)
|
||||
}
|
||||
}
|
||||
|
|
@ -11,7 +11,6 @@ import com.tangem.datasource.api.tangemTech.models.StartReferralBody
|
|||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.domain.wallets.models.requireColdWallet
|
||||
import com.tangem.feature.referral.converters.ReferralConverter
|
||||
import com.tangem.feature.referral.domain.ReferralRepository
|
||||
import com.tangem.feature.referral.domain.models.ReferralData
|
||||
|
|
@ -97,13 +96,13 @@ internal class ReferralRepositoryImpl @Inject constructor(
|
|||
sdkToken = sdkToken,
|
||||
blockchain = blockchain,
|
||||
extraDerivationPath = null,
|
||||
scanResponse = userWallet.requireColdWallet().scanResponse, // TODO [REDACTED_TASK_KEY]
|
||||
userWallet = userWallet,
|
||||
)
|
||||
} else {
|
||||
cryptoCurrencyFactory.createCoin(
|
||||
blockchain = blockchain,
|
||||
extraDerivationPath = null,
|
||||
scanResponse = userWallet.requireColdWallet().scanResponse, // TODO [REDACTED_TASK_KEY]
|
||||
userWallet = userWallet,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ dependencies {
|
|||
implementation(projects.core.utils)
|
||||
implementation(projects.core.ui)
|
||||
implementation(projects.core.decompose)
|
||||
implementation(projects.core.deepLinks)
|
||||
implementation(projects.libs.crypto)
|
||||
implementation(projects.common.routing)
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,10 @@ android {
|
|||
namespace = "com.tangem.features.send.v2.api"
|
||||
}
|
||||
|
||||
tasks.withType<Test>().configureEach {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
/** Core */
|
||||
implementation(projects.core.decompose)
|
||||
|
|
@ -34,4 +38,14 @@ dependencies {
|
|||
/** Other */
|
||||
implementation(deps.arrow.core)
|
||||
implementation(deps.kotlin.immutable.collections)
|
||||
|
||||
// region Tests
|
||||
testImplementation(deps.test.coroutine)
|
||||
testImplementation(deps.test.junit5)
|
||||
testRuntimeOnly(deps.test.junit5.engine)
|
||||
testImplementation(deps.test.mockk)
|
||||
testImplementation(deps.test.truth)
|
||||
testImplementation(projects.common.test)
|
||||
testImplementation(projects.domain.staking.models)
|
||||
// endregion
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.features.send.v2.api
|
||||
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.v2.api.params.FeeSelectorParams
|
||||
|
|
@ -9,5 +9,11 @@ interface FeeSelectorBlockComponent : ComposableContentComponent {
|
|||
|
||||
fun updateState(feeSelectorUM: FeeSelectorUM)
|
||||
|
||||
interface Factory : ComponentFactory<FeeSelectorParams.FeeSelectorBlockParams, FeeSelectorBlockComponent>
|
||||
interface Factory {
|
||||
fun create(
|
||||
context: AppComponentContext,
|
||||
params: FeeSelectorParams.FeeSelectorBlockParams,
|
||||
onResult: (feeSelectorUM: FeeSelectorUM) -> Unit,
|
||||
): FeeSelectorBlockComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,15 @@
|
|||
package com.tangem.features.send.v2.api
|
||||
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
|
||||
import com.tangem.features.send.v2.api.params.FeeSelectorParams
|
||||
|
||||
interface FeeSelectorComponent : ComposableBottomSheetComponent {
|
||||
interface Factory : ComponentFactory<FeeSelectorParams.FeeSelectorDetailsParams, FeeSelectorComponent>
|
||||
interface Factory {
|
||||
fun create(
|
||||
context: AppComponentContext,
|
||||
params: FeeSelectorParams.FeeSelectorDetailsParams,
|
||||
onDismiss: () -> Unit,
|
||||
): FeeSelectorComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -14,7 +14,12 @@ interface SendComponent : ComposableContentComponent {
|
|||
val amount: String? = null,
|
||||
val tag: String? = null,
|
||||
val destinationAddress: String? = null,
|
||||
val callback: ModelCallback? = null,
|
||||
)
|
||||
|
||||
interface Factory : ComponentFactory<Params, SendComponent>
|
||||
|
||||
interface ModelCallback {
|
||||
fun onConvertToAnotherToken(lastAmount: String)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package com.tangem.features.send.v2.api
|
||||
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
|
||||
interface SendEntryPointComponent : ComposableContentComponent {
|
||||
|
||||
data class Params(
|
||||
val userWalletId: UserWalletId,
|
||||
val cryptoCurrency: CryptoCurrency,
|
||||
)
|
||||
|
||||
interface Factory : ComponentFactory<Params, SendEntryPointComponent>
|
||||
}
|
||||
|
|
@ -3,4 +3,5 @@ package com.tangem.features.send.v2.api
|
|||
interface SendFeatureToggles {
|
||||
|
||||
val isSendRedesignEnabled: Boolean
|
||||
val isSendWithSwapEnabled: Boolean
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.tangem.features.send.v2.api.entity
|
|||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.transaction.error.GetFeeError
|
||||
|
|
@ -17,13 +18,12 @@ sealed class FeeSelectorUM {
|
|||
data class Error(val error: GetFeeError) : FeeSelectorUM()
|
||||
|
||||
data class Content(
|
||||
val fees: TransactionFee,
|
||||
val feeItems: ImmutableList<FeeItem>,
|
||||
val selectedFeeItem: FeeItem,
|
||||
val isFeeApproximate: Boolean,
|
||||
val feeExtraInfo: FeeExtraInfo,
|
||||
val feeFiatRateUM: FeeFiatRateUM?,
|
||||
val displayNonceInput: Boolean,
|
||||
val nonce: BigInteger?,
|
||||
val onNonceChange: (String) -> Unit,
|
||||
val feeNonce: FeeNonce,
|
||||
) : FeeSelectorUM()
|
||||
}
|
||||
|
||||
|
|
@ -33,11 +33,26 @@ data class FeeFiatRateUM(
|
|||
val appCurrency: AppCurrency,
|
||||
)
|
||||
|
||||
@Immutable
|
||||
data class FeeExtraInfo(
|
||||
val isFeeApproximate: Boolean,
|
||||
val isFeeConvertibleToFiat: Boolean,
|
||||
val isTronToken: Boolean,
|
||||
)
|
||||
|
||||
sealed class FeeNonce {
|
||||
data object None : FeeNonce()
|
||||
data class Nonce(
|
||||
val nonce: BigInteger?,
|
||||
val onNonceChange: (String) -> Unit,
|
||||
) : FeeNonce()
|
||||
}
|
||||
|
||||
@Immutable
|
||||
sealed class FeeItem {
|
||||
abstract val fee: Fee
|
||||
|
||||
fun isSame(other: FeeItem): Boolean {
|
||||
fun isSameClass(other: FeeItem): Boolean {
|
||||
return this::class == other::class
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.features.send.v2.api.feeselector
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
data class FeeSelectorReloadData(val removeSuggestedFee: Boolean = false)
|
||||
|
||||
interface FeeSelectorReloadTrigger {
|
||||
suspend fun triggerUpdate(data: FeeSelectorReloadData = FeeSelectorReloadData())
|
||||
}
|
||||
|
||||
interface FeeSelectorReloadListener {
|
||||
val reloadTriggerFlow: Flow<FeeSelectorReloadData>
|
||||
}
|
||||
|
|
@ -13,27 +13,36 @@ sealed class FeeSelectorParams {
|
|||
abstract val state: FeeSelectorUM
|
||||
abstract val onLoadFee: suspend () -> Either<GetFeeError, TransactionFee>
|
||||
abstract val cryptoCurrencyStatus: CryptoCurrencyStatus
|
||||
abstract val callback: FeeSelectorModelCallback
|
||||
abstract val feeCryptoCurrencyStatus: CryptoCurrencyStatus
|
||||
abstract val suggestedFeeState: SuggestedFeeState
|
||||
abstract val feeDisplaySource: FeeDisplaySource
|
||||
|
||||
data class FeeSelectorBlockParams(
|
||||
override val state: FeeSelectorUM,
|
||||
override val onLoadFee: suspend () -> Either<GetFeeError, TransactionFee>,
|
||||
override val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
override val callback: FeeSelectorModelCallback,
|
||||
override val feeCryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
override val suggestedFeeState: SuggestedFeeState,
|
||||
override val feeDisplaySource: FeeDisplaySource,
|
||||
) : FeeSelectorParams()
|
||||
|
||||
data class FeeSelectorDetailsParams(
|
||||
override val state: FeeSelectorUM,
|
||||
override val onLoadFee: suspend () -> Either<GetFeeError, TransactionFee>,
|
||||
override val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
override val callback: FeeSelectorModelCallback,
|
||||
override val feeCryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
override val suggestedFeeState: SuggestedFeeState,
|
||||
override val feeDisplaySource: FeeDisplaySource,
|
||||
val callback: FeeSelectorModelCallback,
|
||||
) : FeeSelectorParams()
|
||||
|
||||
sealed class SuggestedFeeState {
|
||||
data object None : SuggestedFeeState()
|
||||
data class Suggestion(val title: TextReference, val fee: Fee) : SuggestedFeeState()
|
||||
}
|
||||
|
||||
enum class FeeDisplaySource {
|
||||
Screen,
|
||||
BottomSheet,
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,19 @@
|
|||
package com.tangem.features.send.v2.api.subcomponents.destination
|
||||
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM
|
||||
|
||||
interface SendDestinationBlockComponent : ComposableContentComponent {
|
||||
|
||||
interface Factory :
|
||||
ComponentFactory<SendDestinationComponentParams.DestinationBlockParams, SendDestinationBlockComponent>
|
||||
fun updateState(destinationUM: DestinationUM)
|
||||
|
||||
interface Factory {
|
||||
fun create(
|
||||
context: AppComponentContext,
|
||||
params: SendDestinationComponentParams.DestinationBlockParams,
|
||||
onClick: () -> Unit,
|
||||
onResult: (DestinationUM) -> Unit,
|
||||
): SendDestinationBlockComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,8 @@ import com.tangem.features.send.v2.api.subcomponents.destination.entity.Destinat
|
|||
|
||||
interface SendDestinationComponent : ComposableContentComponent {
|
||||
|
||||
fun updateState(destinationUM: DestinationUM)
|
||||
|
||||
interface ModelCallback : NavigationModelCallback {
|
||||
fun onDestinationResult(destinationUM: DestinationUM)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,99 @@
|
|||
package com.tangem.features.send.v2.api.subcomponents.feeSelector.utils
|
||||
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.core.ui.utils.parseBigDecimal
|
||||
import com.tangem.core.ui.utils.parseToBigDecimal
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.v2.api.entity.FeeItem
|
||||
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
|
||||
import com.tangem.utils.extensions.isZero
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
|
||||
object FeeCalculationUtils {
|
||||
|
||||
private val FEE_MAX_DIFF = BigDecimal("5")
|
||||
|
||||
/**
|
||||
* Check and calculates subtracted amount
|
||||
*/
|
||||
fun checkAndCalculateSubtractedAmount(
|
||||
isAmountSubtractAvailable: Boolean,
|
||||
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
amountValue: BigDecimal,
|
||||
feeValue: BigDecimal,
|
||||
reduceAmountBy: BigDecimal,
|
||||
): BigDecimal {
|
||||
val balance = cryptoCurrencyStatus.value.amount ?: return amountValue
|
||||
val isFeeCoverage = checkFeeCoverage(
|
||||
isSubtractAvailable = isAmountSubtractAvailable,
|
||||
balance = balance,
|
||||
amountValue = amountValue,
|
||||
feeValue = feeValue,
|
||||
reduceAmountBy = reduceAmountBy,
|
||||
)
|
||||
return if (isFeeCoverage) {
|
||||
balance.minus(reduceAmountBy).minus(feeValue)
|
||||
} else {
|
||||
amountValue
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if custom fee is too high
|
||||
*/
|
||||
fun checkIfCustomFeeTooHigh(feeSelectorUM: FeeSelectorUM.Content): Pair<Boolean, String> {
|
||||
val defaultResult = false to ""
|
||||
|
||||
if (feeSelectorUM.selectedFeeItem !is FeeItem.Custom) return defaultResult
|
||||
|
||||
val customAmount = feeSelectorUM.selectedFeeItem.customValues.firstOrNull() ?: return defaultResult
|
||||
val multipleFees = feeSelectorUM.fees as? TransactionFee.Choosable ?: return defaultResult
|
||||
val highValue = multipleFees.priority.amount.value ?: return defaultResult
|
||||
|
||||
val customValue = customAmount.value.parseToBigDecimal(customAmount.decimals)
|
||||
val diff = if (highValue > BigDecimal.ZERO) {
|
||||
customValue / highValue
|
||||
} else {
|
||||
BigDecimal.ZERO
|
||||
}
|
||||
val isFeeTooHigh = diff > FEE_MAX_DIFF
|
||||
return isFeeTooHigh to diff.parseBigDecimal(0, RoundingMode.HALF_UP)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if custom fee is too low
|
||||
*/
|
||||
fun checkIfCustomFeeTooLow(feeSelectorUM: FeeSelectorUM.Content): Boolean {
|
||||
if (feeSelectorUM.selectedFeeItem !is FeeItem.Custom) return false
|
||||
|
||||
val multipleFees = feeSelectorUM.fees as? TransactionFee.Choosable ?: return false
|
||||
val minimumValue = multipleFees.minimum.amount.value ?: return false
|
||||
val customAmount = feeSelectorUM.selectedFeeItem.customValues.firstOrNull() ?: return false
|
||||
val customValue = customAmount.value.parseToBigDecimal(customAmount.decimals)
|
||||
|
||||
return minimumValue > customValue
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if sending amount with fee is greater than balance
|
||||
*/
|
||||
fun checkFeeCoverage(
|
||||
isSubtractAvailable: Boolean,
|
||||
balance: BigDecimal,
|
||||
amountValue: BigDecimal,
|
||||
feeValue: BigDecimal,
|
||||
reduceAmountBy: BigDecimal?,
|
||||
): Boolean {
|
||||
if (!isSubtractAvailable) return false
|
||||
val reducedBy = balance - (reduceAmountBy ?: BigDecimal.ZERO)
|
||||
return reducedBy < amountValue + feeValue && reducedBy > feeValue && reducedBy >= amountValue
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if fee exceeds fee paid currency balance
|
||||
*/
|
||||
fun checkExceedBalance(feeBalance: BigDecimal?, feeAmount: BigDecimal?): Boolean {
|
||||
return feeAmount == null || feeBalance == null || feeAmount.isZero() || feeAmount > feeBalance
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.features.send.v2.api.subcomponents.notifications
|
||||
|
||||
import com.tangem.features.send.v2.api.SendNotificationsComponent.Params.NotificationData
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface SendNotificationsUpdateListener {
|
||||
/** Flow triggers notifications update */
|
||||
val updateTriggerFlow: Flow<NotificationData>
|
||||
|
||||
/** Flow returns whether there is error notifications */
|
||||
val hasErrorFlow: Flow<Boolean>
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.features.send.v2.api.subcomponents.notifications
|
||||
|
||||
import com.tangem.features.send.v2.api.SendNotificationsComponent.Params.NotificationData
|
||||
|
||||
interface SendNotificationsUpdateTrigger {
|
||||
/** Trigger return callback with check result */
|
||||
suspend fun callbackHasError(hasError: Boolean)
|
||||
|
||||
/** Trigger fee check reload */
|
||||
suspend fun triggerUpdate(data: NotificationData)
|
||||
}
|
||||
|
|
@ -0,0 +1,188 @@
|
|||
package com.tangem.features.send.v2.api.subcomponents.feeSelector.utils
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import io.mockk.mockk
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.math.BigDecimal
|
||||
|
||||
class FeeCalculationUtilsTest {
|
||||
|
||||
@Test
|
||||
fun `GIVEN amount subtract available and fee coverage needed WHEN checkAndCalculateSubtractedAmount THEN returns subtracted amount`() {
|
||||
// GIVEN
|
||||
val isAmountSubtractAvailable = true
|
||||
val cryptoCurrencyStatus = createCryptoCurrencyStatus(BigDecimal("6"))
|
||||
val amountValue = BigDecimal("5")
|
||||
val feeValue = BigDecimal("2")
|
||||
val reduceAmountBy = BigDecimal("1")
|
||||
|
||||
// WHEN
|
||||
val result = FeeCalculationUtils.checkAndCalculateSubtractedAmount(
|
||||
isAmountSubtractAvailable = isAmountSubtractAvailable,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
amountValue = amountValue,
|
||||
feeValue = feeValue,
|
||||
reduceAmountBy = reduceAmountBy,
|
||||
)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isEqualTo(BigDecimal("3"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN amount subtract not available WHEN checkAndCalculateSubtractedAmount THEN returns original amount`() {
|
||||
// GIVEN
|
||||
val isAmountSubtractAvailable = false
|
||||
val cryptoCurrencyStatus = createCryptoCurrencyStatus(BigDecimal("10"))
|
||||
val amountValue = BigDecimal("5")
|
||||
val feeValue = BigDecimal("2")
|
||||
val reduceAmountBy = BigDecimal("1")
|
||||
|
||||
// WHEN
|
||||
val result = FeeCalculationUtils.checkAndCalculateSubtractedAmount(
|
||||
isAmountSubtractAvailable = isAmountSubtractAvailable,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
amountValue = amountValue,
|
||||
feeValue = feeValue,
|
||||
reduceAmountBy = reduceAmountBy,
|
||||
)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isEqualTo(amountValue)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN sufficient balance for amount and fee WHEN checkAndCalculateSubtractedAmount THEN returns original amount`() {
|
||||
// GIVEN
|
||||
val isAmountSubtractAvailable = true
|
||||
val cryptoCurrencyStatus = createCryptoCurrencyStatus(BigDecimal("10"))
|
||||
val amountValue = BigDecimal("5")
|
||||
val feeValue = BigDecimal("2")
|
||||
val reduceAmountBy = BigDecimal("1")
|
||||
|
||||
// WHEN
|
||||
val result = FeeCalculationUtils.checkAndCalculateSubtractedAmount(
|
||||
isAmountSubtractAvailable = isAmountSubtractAvailable,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
amountValue = amountValue,
|
||||
feeValue = feeValue,
|
||||
reduceAmountBy = reduceAmountBy,
|
||||
)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isEqualTo(amountValue)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN no balance WHEN checkAndCalculateSubtractedAmount THEN returns original amount`() {
|
||||
// GIVEN
|
||||
val isAmountSubtractAvailable = true
|
||||
val cryptoCurrencyStatus = createCryptoCurrencyStatus(null)
|
||||
val amountValue = BigDecimal("5")
|
||||
val feeValue = BigDecimal("2")
|
||||
val reduceAmountBy = BigDecimal("1")
|
||||
|
||||
// WHEN
|
||||
val result = FeeCalculationUtils.checkAndCalculateSubtractedAmount(
|
||||
isAmountSubtractAvailable = isAmountSubtractAvailable,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
amountValue = amountValue,
|
||||
feeValue = feeValue,
|
||||
reduceAmountBy = reduceAmountBy,
|
||||
)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isEqualTo(amountValue)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN fee exceeds balance WHEN checkExceedBalance THEN returns true`() {
|
||||
// GIVEN
|
||||
val feeBalance = BigDecimal("5")
|
||||
val feeAmount = BigDecimal("10")
|
||||
|
||||
// WHEN
|
||||
val result = FeeCalculationUtils.checkExceedBalance(feeBalance, feeAmount)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN fee within balance WHEN checkExceedBalance THEN returns false`() {
|
||||
// GIVEN
|
||||
val feeBalance = BigDecimal("10")
|
||||
val feeAmount = BigDecimal("5")
|
||||
|
||||
// WHEN
|
||||
val result = FeeCalculationUtils.checkExceedBalance(feeBalance, feeAmount)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN null fee amount WHEN checkExceedBalance THEN returns true`() {
|
||||
// GIVEN
|
||||
val feeBalance = BigDecimal("10")
|
||||
val feeAmount: BigDecimal? = null
|
||||
|
||||
// WHEN
|
||||
val result = FeeCalculationUtils.checkExceedBalance(feeBalance, feeAmount)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN null fee balance WHEN checkExceedBalance THEN returns true`() {
|
||||
// GIVEN
|
||||
val feeBalance: BigDecimal? = null
|
||||
val feeAmount = BigDecimal("5")
|
||||
|
||||
// WHEN
|
||||
val result = FeeCalculationUtils.checkExceedBalance(feeBalance, feeAmount)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN zero fee amount WHEN checkExceedBalance THEN returns true`() {
|
||||
// GIVEN
|
||||
val feeBalance = BigDecimal("10")
|
||||
val feeAmount = BigDecimal.ZERO
|
||||
|
||||
// WHEN
|
||||
val result = FeeCalculationUtils.checkExceedBalance(feeBalance, feeAmount)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isTrue()
|
||||
}
|
||||
|
||||
private fun createCryptoCurrencyStatus(amount: BigDecimal?): CryptoCurrencyStatus {
|
||||
val value = if (amount != null) {
|
||||
CryptoCurrencyStatus.Loaded(
|
||||
amount = amount,
|
||||
fiatAmount = BigDecimal.ZERO,
|
||||
fiatRate = BigDecimal.ZERO,
|
||||
priceChange = BigDecimal.ZERO,
|
||||
yieldBalance = null,
|
||||
hasCurrentNetworkTransactions = false,
|
||||
pendingTransactions = emptySet(),
|
||||
networkAddress = mockk(relaxed = true),
|
||||
sources = CryptoCurrencyStatus.Sources(),
|
||||
)
|
||||
} else {
|
||||
CryptoCurrencyStatus.NoAmount(
|
||||
priceChange = BigDecimal.ZERO,
|
||||
fiatRate = BigDecimal.ZERO,
|
||||
)
|
||||
}
|
||||
return CryptoCurrencyStatus(
|
||||
currency = mockk(relaxed = true),
|
||||
value = value,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -11,11 +11,16 @@ android {
|
|||
namespace = "com.tangem.features.send.v2.impl"
|
||||
}
|
||||
|
||||
tasks.withType<Test>().configureEach {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
/** Api */
|
||||
implementation(projects.features.sendV2.api)
|
||||
implementation(projects.features.txhistory.api)
|
||||
implementation(projects.features.nft.api)
|
||||
implementation(projects.features.swapV2.api)
|
||||
|
||||
/** Libs */
|
||||
implementation(projects.libs.crypto)
|
||||
|
|
@ -60,6 +65,7 @@ dependencies {
|
|||
implementation(projects.domain.nft.models)
|
||||
implementation(projects.domain.nft)
|
||||
implementation(projects.domain.notifications)
|
||||
implementation(projects.domain.swap.models)
|
||||
|
||||
|
||||
/** Compose libraries */
|
||||
|
|
@ -79,4 +85,13 @@ dependencies {
|
|||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
|
||||
// region Tests
|
||||
testImplementation(deps.test.coroutine)
|
||||
testImplementation(deps.test.junit5)
|
||||
testRuntimeOnly(deps.test.junit5.engine)
|
||||
testImplementation(deps.test.mockk)
|
||||
testImplementation(deps.test.truth)
|
||||
testImplementation(projects.common.test)
|
||||
// endregion
|
||||
}
|
||||
|
|
@ -8,4 +8,6 @@ internal class DefaultSendFeatureToggles(
|
|||
) : SendFeatureToggles {
|
||||
override val isSendRedesignEnabled: Boolean
|
||||
get() = featureToggles.isFeatureEnabled("SEND_REDESIGN_ENABLED")
|
||||
override val isSendWithSwapEnabled: Boolean
|
||||
get() = featureToggles.isFeatureEnabled("SEND_VIA_SWAP_ENABLED")
|
||||
}
|
||||
|
|
@ -18,6 +18,11 @@ internal sealed class CommonSendRoute : Route {
|
|||
override val isEditMode: Boolean = true
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data object ConfirmSuccess : CommonSendRoute() {
|
||||
override val isEditMode: Boolean = false
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Destination(
|
||||
override val isEditMode: Boolean,
|
||||
|
|
|
|||
|
|
@ -6,8 +6,7 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.arkivanov.decompose.extensions.compose.stack.Children
|
||||
import com.arkivanov.decompose.extensions.compose.stack.animation.slide
|
||||
import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation
|
||||
import com.arkivanov.decompose.extensions.compose.stack.animation.*
|
||||
import com.arkivanov.decompose.router.stack.ChildStack
|
||||
import com.tangem.common.ui.navigationButtons.NavigationUM
|
||||
import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon
|
||||
|
|
@ -15,6 +14,8 @@ import com.tangem.core.ui.decompose.ComposableContentComponent
|
|||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.send.v2.common.CommonSendRoute
|
||||
import com.tangem.features.send.v2.send.confirm.SendConfirmComponent
|
||||
import com.tangem.features.send.v2.send.success.SendConfirmSuccessComponent
|
||||
|
||||
@Composable
|
||||
internal fun SendContent(
|
||||
|
|
@ -22,22 +23,30 @@ internal fun SendContent(
|
|||
stackState: ChildStack<CommonSendRoute, ComposableContentComponent>,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.Companion
|
||||
modifier = Modifier
|
||||
.background(color = TangemTheme.colors.background.tertiary)
|
||||
.fillMaxSize()
|
||||
.imePadding()
|
||||
.systemBarsPadding(),
|
||||
horizontalAlignment = Alignment.Companion.CenterHorizontally,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
SendAppBar(navigationUM = navigationUM)
|
||||
Children(
|
||||
stack = stackState,
|
||||
animation = stackAnimation(slide()),
|
||||
animation = stackAnimation { child ->
|
||||
when (child.instance) {
|
||||
is SendConfirmSuccessComponent -> fade(minAlpha = 1.0f)
|
||||
is SendConfirmComponent -> fade()
|
||||
else -> slide()
|
||||
}
|
||||
},
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
it.instance.Content(Modifier.weight(1f))
|
||||
}
|
||||
SendNavigationButtons(navigationUM = navigationUM)
|
||||
if (stackState.active.configuration != CommonSendRoute.ConfirmSuccess) {
|
||||
SendNavigationButtons(navigationUM = navigationUM)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,10 +2,8 @@ package com.tangem.features.send.v2.di
|
|||
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import com.tangem.features.send.v2.DefaultSendFeatureToggles
|
||||
import com.tangem.features.send.v2.api.NFTSendComponent
|
||||
import com.tangem.features.send.v2.api.SendComponent
|
||||
import com.tangem.features.send.v2.api.SendFeatureToggles
|
||||
import com.tangem.features.send.v2.api.SendNotificationsComponent
|
||||
import com.tangem.features.send.v2.api.*
|
||||
import com.tangem.features.send.v2.entrypoint.DefaultSendEntryPointComponent
|
||||
import com.tangem.features.send.v2.send.DefaultSendComponent
|
||||
import com.tangem.features.send.v2.sendnft.DefaultNFTSendComponent
|
||||
import com.tangem.features.send.v2.subcomponents.notifications.DefaultSendNotificationsComponent
|
||||
|
|
@ -43,4 +41,10 @@ internal interface SendFeatureModuleBinds {
|
|||
fun provideNotificationComponentFactory(
|
||||
impl: DefaultSendNotificationsComponent.Factory,
|
||||
): SendNotificationsComponent.Factory
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun provideSendEntryPointComponentFactory(
|
||||
impl: DefaultSendEntryPointComponent.Factory,
|
||||
): SendEntryPointComponent.Factory
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
package com.tangem.features.send.v2.entrypoint
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
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.context.child
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.features.send.v2.api.SendComponent
|
||||
import com.tangem.features.send.v2.api.SendEntryPointComponent
|
||||
import com.tangem.features.send.v2.entrypoint.model.SendEntryPoint
|
||||
import com.tangem.features.send.v2.entrypoint.model.SendEntryPointModel
|
||||
import com.tangem.features.swap.v2.api.SendWithSwapComponent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
internal class DefaultSendEntryPointComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted private val params: SendEntryPointComponent.Params,
|
||||
sendWithSwapComponentFactory: SendWithSwapComponent.Factory,
|
||||
sendComponentFactory: SendComponent.Factory,
|
||||
) : SendEntryPointComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: SendEntryPointModel = getOrCreateModel(params = params)
|
||||
|
||||
private val sendWithSwapComponent = sendWithSwapComponentFactory.create(
|
||||
context = child("sendEntrySendWithSwap"),
|
||||
params = SendWithSwapComponent.Params(
|
||||
userWalletId = params.userWalletId,
|
||||
currency = params.cryptoCurrency,
|
||||
callback = model,
|
||||
),
|
||||
)
|
||||
|
||||
private val sendComponent = sendComponentFactory.create(
|
||||
context = child("sendEntryVanillaSend"),
|
||||
params = SendComponent.Params(
|
||||
userWalletId = params.userWalletId,
|
||||
currency = params.cryptoCurrency,
|
||||
callback = model,
|
||||
),
|
||||
)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val sendEntryState by model.sendEntryPointState.collectAsStateWithLifecycle()
|
||||
|
||||
sendComponent.Content(modifier)
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = sendEntryState == SendEntryPoint.SendWithSwap,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
) {
|
||||
sendWithSwapComponent.Content(modifier)
|
||||
}
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : SendEntryPointComponent.Factory {
|
||||
override fun create(
|
||||
context: AppComponentContext,
|
||||
params: SendEntryPointComponent.Params,
|
||||
): DefaultSendEntryPointComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.features.send.v2.entrypoint.di
|
||||
|
||||
import com.tangem.core.decompose.di.ModelComponent
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.features.send.v2.entrypoint.model.SendEntryPointModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.multibindings.ClassKey
|
||||
import dagger.multibindings.IntoMap
|
||||
|
||||
@Module
|
||||
@InstallIn(ModelComponent::class)
|
||||
internal interface SendEntryPointModule {
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(SendEntryPointModel::class)
|
||||
fun provideSendEntryPointModel(model: SendEntryPointModel): Model
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
package com.tangem.features.send.v2.entrypoint.model
|
||||
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.features.send.v2.api.SendComponent
|
||||
import com.tangem.features.send.v2.api.SendEntryPointComponent
|
||||
import com.tangem.features.send.v2.subcomponents.amount.SendAmountUpdateTrigger
|
||||
import com.tangem.features.swap.v2.api.SendWithSwapComponent
|
||||
import com.tangem.features.swap.v2.api.choosetoken.SwapChooseTokenNetworkListener
|
||||
import com.tangem.features.swap.v2.api.subcomponents.SwapAmountUpdateTrigger
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
import com.tangem.utils.coroutines.saveIn
|
||||
import jakarta.inject.Inject
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@ModelScoped
|
||||
internal class SendEntryPointModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
val appRouter: AppRouter,
|
||||
private val swapChooseTokenNetworkListener: SwapChooseTokenNetworkListener,
|
||||
private val sendAmountUpdateTrigger: SendAmountUpdateTrigger,
|
||||
private val swapAmountUpdateTrigger: SwapAmountUpdateTrigger,
|
||||
) : Model(), SendComponent.ModelCallback, SendWithSwapComponent.ModelCallback {
|
||||
|
||||
private val params: SendEntryPointComponent.Params = paramsContainer.require()
|
||||
|
||||
val sendEntryPointState: StateFlow<SendEntryPoint>
|
||||
field = MutableStateFlow(SendEntryPoint.SendVanilla)
|
||||
|
||||
private var swapChooseTokenListenerJobHolder = JobHolder()
|
||||
|
||||
override fun onConvertToAnotherToken(lastAmount: String) {
|
||||
appRouter.push(
|
||||
AppRoute.ChooseManagedTokens(
|
||||
userWalletId = params.userWalletId,
|
||||
initialCurrency = params.cryptoCurrency,
|
||||
source = AppRoute.ChooseManagedTokens.Source.SendViaSwap,
|
||||
),
|
||||
)
|
||||
observeChooseSelectToken(lastAmount)
|
||||
}
|
||||
|
||||
override fun onCloseSwap(lastAmount: String) {
|
||||
modelScope.launch {
|
||||
if (lastAmount.isNotBlank()) {
|
||||
sendAmountUpdateTrigger.triggerUpdateAmount(lastAmount)
|
||||
}
|
||||
triggerScreenUpdate(SendEntryPoint.SendVanilla)
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeChooseSelectToken(lastAmount: String) {
|
||||
swapChooseTokenNetworkListener.swapChooseTokenNetworkResultFlow
|
||||
.onEach { currency ->
|
||||
if (lastAmount.isNotBlank()) {
|
||||
swapAmountUpdateTrigger.triggerUpdateAmount(lastAmount)
|
||||
}
|
||||
triggerScreenUpdate(SendEntryPoint.SendWithSwap)
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
.saveIn(swapChooseTokenListenerJobHolder)
|
||||
}
|
||||
|
||||
private fun triggerScreenUpdate(entry: SendEntryPoint) {
|
||||
swapChooseTokenListenerJobHolder.cancel()
|
||||
sendEntryPointState.update { entry }
|
||||
}
|
||||
}
|
||||
|
||||
enum class SendEntryPoint {
|
||||
SendVanilla,
|
||||
SendWithSwap,
|
||||
}
|
||||
|
|
@ -1,62 +1,66 @@
|
|||
package com.tangem.features.send.v2.feeselector
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Devices
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.common.ui.amountScreen.utils.getFiatString
|
||||
import com.arkivanov.decompose.extensions.compose.subscribeAsState
|
||||
import com.arkivanov.decompose.router.slot.activate
|
||||
import com.arkivanov.decompose.router.slot.childSlot
|
||||
import com.arkivanov.decompose.router.slot.dismiss
|
||||
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.ui.components.SpacerWMax
|
||||
import com.tangem.core.ui.components.TextShimmer
|
||||
import com.tangem.core.ui.components.atoms.text.EllipsisText
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.EMPTY_BALANCE_SIGN
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.fee
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.core.ui.extensions.conditional
|
||||
import com.tangem.features.send.v2.api.FeeSelectorBlockComponent
|
||||
import com.tangem.features.send.v2.api.entity.FeeFiatRateUM
|
||||
import com.tangem.features.send.v2.api.entity.FeeItem
|
||||
import com.tangem.features.send.v2.api.FeeSelectorComponent
|
||||
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.v2.api.params.FeeSelectorParams
|
||||
import com.tangem.features.send.v2.feeselector.model.FeeSelectorModel
|
||||
import com.tangem.features.send.v2.impl.R
|
||||
import com.tangem.features.send.v2.feeselector.ui.FeeSelectorBlockContent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import java.math.BigDecimal
|
||||
import kotlinx.serialization.builtins.serializer
|
||||
|
||||
internal class DefaultFeeSelectorBlockComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted params: FeeSelectorParams.FeeSelectorBlockParams,
|
||||
@Assisted private val params: FeeSelectorParams.FeeSelectorBlockParams,
|
||||
@Assisted onResult: (feeSelectorUM: FeeSelectorUM) -> Unit,
|
||||
private val feeSelectorComponentFactory: FeeSelectorComponent.Factory,
|
||||
) : FeeSelectorBlockComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: FeeSelectorModel = getOrCreateModel(params = params)
|
||||
|
||||
private val bottomSheetSlot = childSlot(
|
||||
source = model.feeSelectorBottomSheet,
|
||||
serializer = Unit.serializer(),
|
||||
handleBackButton = false,
|
||||
childFactory = { _, componentContext ->
|
||||
feeSelectorComponentFactory.create(
|
||||
context = childByContext(componentContext),
|
||||
params = FeeSelectorParams.FeeSelectorDetailsParams(
|
||||
state = model.uiState.value,
|
||||
onLoadFee = params.onLoadFee,
|
||||
feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus,
|
||||
cryptoCurrencyStatus = params.cryptoCurrencyStatus,
|
||||
callback = model,
|
||||
suggestedFeeState = FeeSelectorParams.SuggestedFeeState.None,
|
||||
feeDisplaySource = FeeSelectorParams.FeeDisplaySource.Screen,
|
||||
),
|
||||
onDismiss = {
|
||||
model.feeSelectorBottomSheet.dismiss()
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
init {
|
||||
model.uiState
|
||||
.onEach(params.callback::onFeeResult)
|
||||
.onEach(onResult)
|
||||
.launchIn(componentScope)
|
||||
}
|
||||
|
||||
|
|
@ -67,7 +71,18 @@ internal class DefaultFeeSelectorBlockComponent @AssistedInject constructor(
|
|||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
FeeSelectorBlockContent(modifier = modifier, state = state)
|
||||
val bottomSheet by bottomSheetSlot.subscribeAsState()
|
||||
|
||||
FeeSelectorBlockContent(
|
||||
state = state,
|
||||
modifier = modifier
|
||||
.conditional(params.feeDisplaySource == FeeSelectorParams.FeeDisplaySource.Screen) {
|
||||
Modifier.clickable {
|
||||
model.feeSelectorBottomSheet.activate(Unit)
|
||||
}
|
||||
},
|
||||
)
|
||||
bottomSheet.child?.instance?.BottomSheet()
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
|
|
@ -75,121 +90,7 @@ internal class DefaultFeeSelectorBlockComponent @AssistedInject constructor(
|
|||
override fun create(
|
||||
context: AppComponentContext,
|
||||
params: FeeSelectorParams.FeeSelectorBlockParams,
|
||||
onResult: (feeSelectorUM: FeeSelectorUM) -> Unit,
|
||||
): DefaultFeeSelectorBlockComponent
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FeeSelectorBlockContent(state: FeeSelectorUM, modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.padding(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(24.dp),
|
||||
painter = painterResource(R.drawable.ic_fee_new_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.accent,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.padding(start = TangemTheme.dimens.spacing4),
|
||||
text = stringResourceSafe(R.string.common_network_fee_title),
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.padding(start = TangemTheme.dimens.spacing6)
|
||||
.size(TangemTheme.dimens.size16),
|
||||
painter = painterResource(id = R.drawable.ic_token_info_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
)
|
||||
SpacerWMax()
|
||||
when (state) {
|
||||
is FeeSelectorUM.Content -> FeeContent(state)
|
||||
is FeeSelectorUM.Loading -> FeeLoading()
|
||||
is FeeSelectorUM.Error -> FeeError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.FeeError() {
|
||||
Text(
|
||||
text = EMPTY_BALANCE_SIGN,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.body2,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.FeeLoading() {
|
||||
TextShimmer(
|
||||
radius = TangemTheme.dimens.radius3,
|
||||
style = TangemTheme.typography.body1,
|
||||
modifier = Modifier.width(width = TangemTheme.dimens.size90),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.FeeContent(state: FeeSelectorUM.Content) {
|
||||
val fiatRate = state.feeFiatRateUM
|
||||
EllipsisText(
|
||||
text = if (fiatRate != null) {
|
||||
getFiatString(
|
||||
value = state.selectedFeeItem.fee.amount.value,
|
||||
rate = fiatRate.rate,
|
||||
appCurrency = fiatRate.appCurrency,
|
||||
approximate = state.isFeeApproximate,
|
||||
)
|
||||
} else {
|
||||
state.selectedFeeItem.fee.amount.value.format {
|
||||
crypto(
|
||||
symbol = state.selectedFeeItem.fee.amount.currencySymbol,
|
||||
decimals = state.selectedFeeItem.fee.amount.decimals,
|
||||
).fee(canBeLower = state.isFeeApproximate)
|
||||
}
|
||||
},
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
textAlign = TextAlign.End,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(start = TangemTheme.dimens.spacing4),
|
||||
)
|
||||
Icon(
|
||||
modifier = Modifier.size(width = 18.dp, height = 24.dp),
|
||||
painter = painterResource(id = R.drawable.ic_select_18_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, device = Devices.PIXEL_7_PRO)
|
||||
@Preview(showBackground = true, device = Devices.PIXEL_7_PRO, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun FeeSelectorBlockContent_Preview() {
|
||||
TangemThemePreview {
|
||||
val feeItem = FeeItem.Market(
|
||||
Fee.Common(amount = Amount(value = BigDecimal("0.0002876"), blockchain = Blockchain.Ethereum)),
|
||||
)
|
||||
FeeSelectorBlockContent(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
state = FeeSelectorUM.Content(
|
||||
feeItems = persistentListOf(feeItem),
|
||||
selectedFeeItem = feeItem,
|
||||
isFeeApproximate = false,
|
||||
feeFiatRateUM = FeeFiatRateUM(
|
||||
rate = BigDecimal("2500"),
|
||||
appCurrency = AppCurrency.Default,
|
||||
),
|
||||
displayNonceInput = false,
|
||||
nonce = null,
|
||||
onNonceChange = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -16,18 +16,24 @@ import dagger.assisted.AssistedInject
|
|||
internal class DefaultFeeSelectorComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted private val params: FeeSelectorParams.FeeSelectorDetailsParams,
|
||||
@Assisted private val onDismiss: () -> Unit,
|
||||
) : FeeSelectorComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: FeeSelectorModel = getOrCreateModel(params = params)
|
||||
|
||||
override fun dismiss() {
|
||||
model.dismiss()
|
||||
onDismiss()
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun BottomSheet() {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
FeeSelectorModalBottomSheet(onDismiss = ::dismiss, state = state, feeSelectorIntents = model)
|
||||
FeeSelectorModalBottomSheet(
|
||||
onDismiss = ::dismiss,
|
||||
state = state,
|
||||
feeSelectorIntents = model,
|
||||
feeDisplaySource = params.feeDisplaySource,
|
||||
)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
|
|
@ -35,6 +41,7 @@ internal class DefaultFeeSelectorComponent @AssistedInject constructor(
|
|||
override fun create(
|
||||
context: AppComponentContext,
|
||||
params: FeeSelectorParams.FeeSelectorDetailsParams,
|
||||
onDismiss: () -> Unit,
|
||||
): DefaultFeeSelectorComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.features.send.v2.feeselector
|
||||
|
||||
import com.tangem.features.send.v2.api.feeselector.FeeSelectorReloadData
|
||||
import com.tangem.features.send.v2.api.feeselector.FeeSelectorReloadListener
|
||||
import com.tangem.features.send.v2.api.feeselector.FeeSelectorReloadTrigger
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
internal class DefaultFeeSelectorReload @Inject constructor() : FeeSelectorReloadListener, FeeSelectorReloadTrigger {
|
||||
private val _reloadTriggerFlow = MutableSharedFlow<FeeSelectorReloadData>()
|
||||
override val reloadTriggerFlow: Flow<FeeSelectorReloadData> = _reloadTriggerFlow.asSharedFlow()
|
||||
|
||||
override suspend fun triggerUpdate(data: FeeSelectorReloadData) {
|
||||
_reloadTriggerFlow.emit(data)
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue