Updated on 2026-08-14
This commit is contained in:
parent
7228c72553
commit
5b60d94e60
29 changed files with 1034 additions and 110 deletions
|
|
@ -12,6 +12,7 @@ import com.tangem.common.extensions.toMapKey
|
|||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.data.common.currency.getNetwork
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.card.BackendId
|
||||
import com.tangem.domain.card.repository.DerivationsRepository
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
|
|
@ -73,15 +74,15 @@ internal class DefaultDerivationsRepository(
|
|||
|
||||
override suspend fun hasMissedDerivations(
|
||||
userWalletId: UserWalletId,
|
||||
networksWithDerivationPath: Map<Network.ID, String?>,
|
||||
networksWithDerivationPath: Map<BackendId, String?>,
|
||||
): Boolean {
|
||||
val userWallet = userWalletsStore.getSyncOrNull(userWalletId) ?: error("User wallet not found")
|
||||
|
||||
val derivations = MissedDerivationsFinder(scanResponse = userWallet.scanResponse)
|
||||
.findByNetworks(
|
||||
networksWithDerivationPath.mapNotNull { (networkId, extraDerivationPath) ->
|
||||
networksWithDerivationPath.mapNotNull { (backendId, extraDerivationPath) ->
|
||||
getNetwork(
|
||||
blockchain = Blockchain.fromNetworkId(networkId.value) ?: return@mapNotNull null,
|
||||
blockchain = Blockchain.fromNetworkId(backendId) ?: return@mapNotNull null,
|
||||
extraDerivationPath = extraDerivationPath,
|
||||
scanResponse = userWallet.scanResponse,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import com.tangem.tap.common.extensions.*
|
|||
import com.tangem.tap.common.redux.AppDialog
|
||||
import com.tangem.tap.common.redux.global.GlobalState
|
||||
import com.tangem.tap.features.demo.DemoHelper
|
||||
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletAction
|
||||
import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
|
||||
import com.tangem.tap.features.saveWallet.redux.SaveWalletAction
|
||||
import com.tangem.tap.mainScope
|
||||
|
|
@ -82,6 +83,56 @@ object OnboardingHelper {
|
|||
}
|
||||
}
|
||||
|
||||
fun saveWallet(
|
||||
scanResponse: ScanResponse,
|
||||
accessCode: String? = null,
|
||||
backupCardsIds: List<String>? = null,
|
||||
hasBackupError: Boolean = false,
|
||||
) {
|
||||
Analytics.setContext(scanResponse)
|
||||
scope.launch {
|
||||
val settingsRepository = store.inject(DaggerGraphState::settingsRepository)
|
||||
when {
|
||||
// When should save user wallets, then save card without navigate to save wallet screen
|
||||
store.inject(DaggerGraphState::walletsRepository).shouldSaveUserWalletsSync() -> {
|
||||
store.dispatchWithMain(
|
||||
SaveWalletAction.ProvideBackupInfo(
|
||||
scanResponse = scanResponse,
|
||||
accessCode = accessCode,
|
||||
backupCardsIds = backupCardsIds?.toSet(),
|
||||
),
|
||||
)
|
||||
|
||||
store.dispatchWithMain(
|
||||
SaveWalletAction.SaveWalletAfterBackup(
|
||||
hasBackupError = hasBackupError,
|
||||
shouldNavigateToWallet = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
// When should not save user wallets but device has biometry and save wallet screen has not been shown,
|
||||
// then open save wallet screen
|
||||
tangemSdkManager.checkCanUseBiometry() && settingsRepository.shouldShowSaveUserWalletScreen() -> {
|
||||
proceedWithScanResponse(scanResponse, backupCardsIds, hasBackupError)
|
||||
|
||||
delay(timeMillis = 1_200)
|
||||
|
||||
store.dispatchOnMain(
|
||||
SaveWalletAction.ProvideBackupInfo(
|
||||
scanResponse = scanResponse,
|
||||
accessCode = accessCode,
|
||||
backupCardsIds = backupCardsIds?.toSet(),
|
||||
),
|
||||
)
|
||||
}
|
||||
// If device has no biometry and save wallet screen has been shown, then go through old scenario
|
||||
else -> {
|
||||
proceedWithScanResponse(scanResponse, backupCardsIds, hasBackupError)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun trySaveWalletAndNavigateToWalletScreen(
|
||||
scanResponse: ScanResponse,
|
||||
accessCode: String? = null,
|
||||
|
|
@ -103,7 +154,12 @@ object OnboardingHelper {
|
|||
),
|
||||
)
|
||||
|
||||
store.dispatchWithMain(SaveWalletAction.SaveWalletAfterBackup(hasBackupError))
|
||||
store.dispatchWithMain(
|
||||
SaveWalletAction.SaveWalletAfterBackup(
|
||||
hasBackupError = hasBackupError,
|
||||
shouldNavigateToWallet = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
// When should not save user wallets but device has biometry and save wallet screen has not been shown,
|
||||
// then open save wallet screen
|
||||
|
|
@ -200,6 +256,7 @@ object OnboardingHelper {
|
|||
}
|
||||
.doOnSuccess {
|
||||
mainScope.launch { store.onUserWalletSelected(userWallet) }
|
||||
store.dispatch(OnboardingWalletAction.WalletSaved(userWallet.walletId))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.tangem.tap.features.onboarding.products.wallet.redux
|
|||
|
||||
import android.net.Uri
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.feature.onboarding.data.model.CreateWalletResponse
|
||||
import com.tangem.feature.onboarding.presentation.wallet2.analytics.SeedPhraseSource
|
||||
import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse
|
||||
|
|
@ -28,6 +29,8 @@ sealed class OnboardingWalletAction : Action {
|
|||
class SetThirdCardArtworkUrl(val artworkUri: Uri?) : OnboardingWalletAction()
|
||||
|
||||
data object OnBackPressed : OnboardingWalletAction()
|
||||
|
||||
data class WalletSaved(val userWalletId: UserWalletId) : OnboardingWalletAction()
|
||||
}
|
||||
|
||||
sealed class OnboardingWallet2Action : OnboardingWalletAction() {
|
||||
|
|
@ -89,6 +92,8 @@ sealed class BackupAction : Action {
|
|||
|
||||
data class FinishBackup(val withAnalytics: Boolean = true) : BackupAction()
|
||||
|
||||
data class BackupFinished(val userWalletId: UserWalletId?) : BackupAction()
|
||||
|
||||
data object DiscardBackup : BackupAction()
|
||||
data object DiscardSavedBackup : BackupAction()
|
||||
data object ResumeFoundUnfinishedBackup : BackupAction()
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import com.google.firebase.crashlytics.FirebaseCrashlytics
|
|||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.card.Card
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.extensions.guard
|
||||
import com.tangem.common.extensions.ifNotNull
|
||||
import com.tangem.common.extensions.toHexString
|
||||
import com.tangem.common.routing.AppRoute
|
||||
|
|
@ -19,6 +18,7 @@ import com.tangem.domain.models.scan.CardDTO
|
|||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.wallets.builder.UserWalletBuilder
|
||||
import com.tangem.domain.wallets.models.Artwork
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.feature.onboarding.data.model.CreateWalletResponse
|
||||
import com.tangem.feature.onboarding.presentation.wallet2.analytics.SeedPhraseSource
|
||||
import com.tangem.feature.wallet.presentation.wallet.domain.BackupValidator
|
||||
|
|
@ -164,16 +164,7 @@ private fun handleWalletAction(action: Action) {
|
|||
}
|
||||
is OnboardingWalletAction.FinishOnboarding -> {
|
||||
store.dispatch(GlobalAction.Onboarding.Stop)
|
||||
|
||||
if (scanResponse == null) {
|
||||
action.scope.launch {
|
||||
readCard { newScanResponse ->
|
||||
handleFinishOnboardind(newScanResponse)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
handleFinishOnboardind(scanResponse)
|
||||
}
|
||||
navigateToWalletScreen()
|
||||
}
|
||||
is OnboardingWalletAction.ResumeBackup -> {
|
||||
val newAction = when (val backupState = backupService.currentState) {
|
||||
|
|
@ -197,10 +188,10 @@ private fun handleWalletAction(action: Action) {
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleFinishOnboardind(scanResponse: ScanResponse) {
|
||||
private fun handleFinishBackup(scanResponse: ScanResponse) {
|
||||
val backupState = store.state.onboardingWalletState.backupState
|
||||
val updatedScanResponse = updateScanResponseAfterBackup(scanResponse, backupState)
|
||||
OnboardingHelper.trySaveWalletAndNavigateToWalletScreen(
|
||||
OnboardingHelper.saveWallet(
|
||||
scanResponse = updatedScanResponse,
|
||||
accessCode = backupState.accessCode,
|
||||
backupCardsIds = backupState.backupCardIds,
|
||||
|
|
@ -208,7 +199,18 @@ private fun handleFinishOnboardind(scanResponse: ScanResponse) {
|
|||
)
|
||||
}
|
||||
|
||||
private suspend fun readCard(onSuccess: (ScanResponse) -> Unit) {
|
||||
private fun navigateToWalletScreen() {
|
||||
mainScope.launch {
|
||||
val settingsRepository = store.inject(DaggerGraphState::settingsRepository)
|
||||
store.dispatchNavigationAction { replaceAll(AppRoute.Wallet) }
|
||||
if (tangemSdkManager.checkCanUseBiometry() && settingsRepository.shouldShowSaveUserWalletScreen()) {
|
||||
delay(timeMillis = 1_800)
|
||||
store.dispatchNavigationAction { push(AppRoute.SaveWallet) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun readCard(onSuccess: suspend (ScanResponse) -> Unit) {
|
||||
val shouldSaveAccessCodes = store.inject(DaggerGraphState::settingsRepository).shouldSaveAccessCodes()
|
||||
|
||||
store.inject(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy(
|
||||
|
|
@ -403,7 +405,7 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
|
|||
val onboardingWalletState = appState()?.onboardingWalletState ?: return
|
||||
|
||||
val backupState = onboardingWalletState.backupState
|
||||
val scanResponse = globalState.onboardingState.onboardingManager?.scanResponse
|
||||
var scanResponse = globalState.onboardingState.onboardingManager?.scanResponse
|
||||
val card = scanResponse?.card
|
||||
|
||||
when (action) {
|
||||
|
|
@ -573,6 +575,15 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
|
|||
cardIds = gatherCardIds(backupState, card),
|
||||
)
|
||||
}
|
||||
if (scanResponse == null) {
|
||||
scope.launch {
|
||||
readCard { newScanResponse ->
|
||||
handleFinishBackup(newScanResponse)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
handleFinishBackup(scanResponse)
|
||||
}
|
||||
}
|
||||
is BackupAction.FinishBackup -> {
|
||||
scope.launch {
|
||||
|
|
@ -580,28 +591,31 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
|
|||
Analytics.send(Onboarding.Backup.Finished(backupState.backupCardsNumber))
|
||||
}
|
||||
|
||||
var userWallet: UserWallet? = null
|
||||
if (scanResponse != null) {
|
||||
val walletNameGenerateUseCase = store.inject(DaggerGraphState::generateWalletNameUseCase)
|
||||
val userWallet = UserWalletBuilder(scanResponse, walletNameGenerateUseCase)
|
||||
.backupCardsIds(backupState.backupCardIds.toSet())
|
||||
.build()
|
||||
.guard {
|
||||
Timber.e("User wallet not created")
|
||||
return@launch
|
||||
}
|
||||
userWallet = createUserWallet(
|
||||
scanResponse = requireNotNull(value = scanResponse, lazyMessage = { "ScanResponse is null" }),
|
||||
backupState = backupState,
|
||||
)
|
||||
|
||||
val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
|
||||
userWalletsListManager.update(
|
||||
userWalletId = userWallet.walletId,
|
||||
update = { wallet ->
|
||||
wallet.copy(
|
||||
scanResponse = updateScanResponseAfterBackup(
|
||||
scanResponse = wallet.scanResponse,
|
||||
backupState = backupState,
|
||||
userWalletsListManager.save(
|
||||
userWallet = userWallet.copy(
|
||||
scanResponse = updateScanResponseAfterBackup(
|
||||
scanResponse = requireNotNull(
|
||||
value = scanResponse,
|
||||
lazyMessage = { "ScanResponse is null" },
|
||||
),
|
||||
)
|
||||
},
|
||||
backupState = backupState,
|
||||
),
|
||||
),
|
||||
canOverride = true,
|
||||
)
|
||||
} else {
|
||||
readCard { newScanResponse ->
|
||||
scanResponse = newScanResponse
|
||||
userWallet = createUserWallet(newScanResponse, backupState)
|
||||
}
|
||||
}
|
||||
|
||||
val notActivatedCardIds = gatherCardIds(backupState, card).mapNotNull {
|
||||
|
|
@ -613,11 +627,18 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
|
|||
}
|
||||
|
||||
// All cardIds may already be activated if the backup was skipped before.
|
||||
if (notActivatedCardIds.isEmpty()) return@launch
|
||||
if (notActivatedCardIds.isEmpty()) {
|
||||
delay(1000)
|
||||
store.dispatch(BackupAction.BackupFinished(userWallet?.walletId))
|
||||
return@launch
|
||||
}
|
||||
|
||||
Analytics.send(Onboarding.Finished())
|
||||
|
||||
store.state.globalState.onboardingState.onboardingManager?.finishActivation(notActivatedCardIds)
|
||||
handleFinishBackup(requireNotNull(scanResponse))
|
||||
delay(1000)
|
||||
store.dispatch(BackupAction.BackupFinished(userWalletId = userWallet?.walletId))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -629,6 +650,16 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun createUserWallet(scanResponse: ScanResponse, backupState: BackupState): UserWallet {
|
||||
val walletNameGenerateUseCase = store.inject(DaggerGraphState::generateWalletNameUseCase)
|
||||
return requireNotNull(
|
||||
value = UserWalletBuilder(scanResponse, walletNameGenerateUseCase)
|
||||
.backupCardsIds(backupState.backupCardIds.toSet())
|
||||
.build(),
|
||||
lazyMessage = { "User wallet not created" },
|
||||
)
|
||||
}
|
||||
|
||||
fun updateArtworks(addedBackupCardsCount: Int, card: Card) {
|
||||
mainScope.launch {
|
||||
withIOContext {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.tap.features.onboarding.products.wallet.redux
|
||||
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.redux.OnboardingManageTokensAction
|
||||
import com.tangem.tap.backupService
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
|
|
@ -10,12 +11,23 @@ object OnboardingWalletReducer {
|
|||
fun reduce(action: Action, state: AppState): OnboardingWalletState = internalReduce(action, state)
|
||||
}
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
private fun internalReduce(action: Action, appState: AppState): OnboardingWalletState {
|
||||
val state = appState.onboardingWalletState
|
||||
|
||||
return when (action) {
|
||||
is GlobalAction.Onboarding -> ReducerForGlobalAction.reduce(action, state)
|
||||
is BackupAction -> state.copy(backupState = BackupReducer.reduce(action, state.backupState))
|
||||
is BackupAction.BackupFinished -> {
|
||||
state.copy(
|
||||
step = if (action.userWalletId != null) {
|
||||
OnboardingWalletStep.ManageTokens
|
||||
} else {
|
||||
OnboardingWalletStep.Done
|
||||
},
|
||||
userWalletId = action.userWalletId,
|
||||
)
|
||||
}
|
||||
is BackupAction -> state.copy(backupState = BackupReducer.reduce(action = action, state = state.backupState))
|
||||
is OnboardingWallet2Action -> OnboardingWallet2Reducer.reduce(action, state)
|
||||
is OnboardingWalletAction.GetToCreateWalletStep -> state.copy(
|
||||
step = OnboardingWalletStep.CreateWallet,
|
||||
|
|
@ -36,6 +48,11 @@ private fun internalReduce(action: Action, appState: AppState): OnboardingWallet
|
|||
walletImages = state.walletImages.copy(thirdCardImage = action.artworkUri),
|
||||
)
|
||||
}
|
||||
is OnboardingManageTokensAction.CurrenciesSaved -> state.copy(step = OnboardingWalletStep.Done)
|
||||
is OnboardingWalletAction.WalletSaved -> state.copy(
|
||||
step = OnboardingWalletStep.ManageTokens,
|
||||
userWalletId = action.userWalletId,
|
||||
)
|
||||
is OnboardingWalletAction.Done -> state.copy(step = OnboardingWalletStep.Done)
|
||||
else -> state
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.tangem.tap.features.onboarding.products.wallet.redux
|
|||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import com.tangem.domain.redux.StateDialog
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import org.rekotlin.StateType
|
||||
|
||||
/**
|
||||
|
|
@ -15,18 +16,19 @@ data class OnboardingWalletState(
|
|||
val walletImages: WalletImages = WalletImages(),
|
||||
val showConfetti: Boolean = false,
|
||||
val isRingOnboarding: Boolean = false,
|
||||
val userWalletId: UserWalletId? = null,
|
||||
) : StateType {
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
fun getMaxProgress(): Int {
|
||||
val baseProgress = 6
|
||||
val baseProgress = 7
|
||||
return getWallet2Progress() + baseProgress
|
||||
}
|
||||
|
||||
@Suppress("ComplexMethod", "MagicNumber")
|
||||
fun getProgressStep(): Int {
|
||||
val progressByStep = when (step) {
|
||||
OnboardingWalletStep.CreateWallet -> 1
|
||||
OnboardingWalletStep.CreateWallet, OnboardingWalletStep.None -> 1
|
||||
OnboardingWalletStep.Backup -> {
|
||||
when (backupState.backupStep) {
|
||||
BackupStep.InitBackup -> 2
|
||||
|
|
@ -36,11 +38,11 @@ data class OnboardingWalletState(
|
|||
BackupStep.ReenterAccessCode -> 4
|
||||
BackupStep.SetAccessCode -> 4
|
||||
BackupStep.WritePrimaryCard, is BackupStep.WriteBackupCard -> 5
|
||||
BackupStep.Finished -> getMaxProgress()
|
||||
BackupStep.Finished -> 6
|
||||
}
|
||||
}
|
||||
OnboardingWalletStep.ManageTokens -> 6
|
||||
OnboardingWalletStep.Done -> getMaxProgress()
|
||||
else -> 1
|
||||
}
|
||||
|
||||
return getWallet2Progress() + progressByStep
|
||||
|
|
@ -60,7 +62,7 @@ data class OnboardingWallet2State(
|
|||
)
|
||||
|
||||
enum class OnboardingWalletStep {
|
||||
None, CreateWallet, Backup, Done
|
||||
None, CreateWallet, Backup, ManageTokens, Done
|
||||
}
|
||||
|
||||
data class BackupState(
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ package com.tangem.tap.features.onboarding.products.wallet.ui
|
|||
import android.content.res.ColorStateList
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
|
|
@ -10,12 +12,16 @@ import android.view.WindowManager
|
|||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.core.view.MenuProvider
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.transition.TransitionManager
|
||||
import by.kirich1409.viewbindingdelegate.viewBinding
|
||||
import coil.load
|
||||
import com.arkivanov.decompose.defaultComponentContext
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import com.tangem.common.CardIdFormatter
|
||||
|
|
@ -25,15 +31,22 @@ import com.tangem.common.routing.AppRoute
|
|||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.Basic
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.context.childByContext
|
||||
import com.tangem.core.decompose.di.RootAppComponentContext
|
||||
import com.tangem.core.ui.extensions.setStatusBarColor
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.windowsize.rememberWindowSize
|
||||
import com.tangem.datasource.utils.isNullOrEmpty
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.feedback.models.FeedbackEmailType
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.feature.onboarding.data.model.CreateWalletResponse
|
||||
import com.tangem.feature.onboarding.presentation.wallet2.analytics.SeedPhraseSource
|
||||
import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.SeedPhraseMediator
|
||||
import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.SeedPhraseRouter
|
||||
import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.SeedPhraseViewModel
|
||||
import com.tangem.features.managetokens.component.OnboardingManageTokensComponent
|
||||
import com.tangem.sdk.ui.widget.leapfrogWidget.LeapfrogWidget
|
||||
import com.tangem.sdk.ui.widget.leapfrogWidget.PropertyCalculator
|
||||
import com.tangem.tap.common.analytics.events.Onboarding
|
||||
|
|
@ -56,6 +69,7 @@ import com.tangem.wallet.databinding.ViewOnboardingProgressBinding
|
|||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.launch
|
||||
import org.rekotlin.StoreSubscriber
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LargeClass", "MagicNumber")
|
||||
@AndroidEntryPoint
|
||||
|
|
@ -64,10 +78,20 @@ class OnboardingWalletFragment :
|
|||
StoreSubscriber<OnboardingWalletState>,
|
||||
FragmentOnBackPressedHandler {
|
||||
|
||||
@Inject
|
||||
internal lateinit var onboardingManageTokensComponentFactory: OnboardingManageTokensComponent.Factory
|
||||
|
||||
@Inject
|
||||
@RootAppComponentContext
|
||||
internal lateinit var rootComponentContext: AppComponentContext
|
||||
|
||||
private var onboardingManageTokensComponent: OnboardingManageTokensComponent? = null
|
||||
|
||||
internal val binding: FragmentOnboardingWalletBinding by viewBinding(FragmentOnboardingWalletBinding::bind)
|
||||
internal val pbBinding: ViewOnboardingProgressBinding by viewBinding(ViewOnboardingProgressBinding::bind)
|
||||
|
||||
internal val bindingSeedPhrase: LayoutOnboardingSeedPhraseBinding by lazy { binding.onboardingSeedPhraseContainer }
|
||||
private val bindingManageTokens by lazy { binding.onboardingManageTokensContainer }
|
||||
|
||||
private val canSkipBackup by lazy { arguments?.getBoolean(AppRoute.OnboardingWallet.CAN_SKIP_BACKUP_KEY) ?: true }
|
||||
|
||||
|
|
@ -173,26 +197,28 @@ class OnboardingWalletFragment :
|
|||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
onboardingManageTokensComponent = null
|
||||
store.unsubscribe(this)
|
||||
}
|
||||
|
||||
override fun newState(state: OnboardingWalletState) {
|
||||
if (activity == null || view == null) return
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
animator.updateBackupState(state.backupState)
|
||||
requireActivity().invalidateOptionsMenu()
|
||||
|
||||
animator.updateBackupState(state.backupState)
|
||||
requireActivity().invalidateOptionsMenu()
|
||||
pbBinding.pbState.max = state.getMaxProgress()
|
||||
pbBinding.pbState.progress = state.getProgressStep()
|
||||
|
||||
pbBinding.pbState.max = state.getMaxProgress()
|
||||
pbBinding.pbState.progress = state.getProgressStep()
|
||||
|
||||
when {
|
||||
state.wallet2State != null -> {
|
||||
seedPhraseStateHandler.newState(this, state, seedPhraseViewModel)
|
||||
updateWalletImagesState(state.walletImages)
|
||||
}
|
||||
else -> {
|
||||
updateWalletImagesState(state.walletImages)
|
||||
handleOnboardingStep(state)
|
||||
when {
|
||||
state.wallet2State != null -> {
|
||||
seedPhraseStateHandler.newState(this, state, seedPhraseViewModel)
|
||||
updateWalletImagesState(state.walletImages)
|
||||
}
|
||||
else -> {
|
||||
updateWalletImagesState(state.walletImages)
|
||||
handleOnboardingStep(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -220,14 +246,53 @@ class OnboardingWalletFragment :
|
|||
internal fun handleOnboardingStep(state: OnboardingWalletState) {
|
||||
when (state.step) {
|
||||
OnboardingWalletStep.CreateWallet -> setupCreateWalletState()
|
||||
OnboardingWalletStep.Backup -> setBackupState(
|
||||
state = state.backupState,
|
||||
)
|
||||
|
||||
OnboardingWalletStep.Backup -> setBackupState(state = state.backupState)
|
||||
OnboardingWalletStep.ManageTokens -> setManageTokensState(requireNotNull(state.userWalletId))
|
||||
OnboardingWalletStep.Done -> showSuccess()
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initializeOnboardingManageTokensComponent(userWalletId: UserWalletId) {
|
||||
val componentContext = rootComponentContext.childByContext(
|
||||
componentContext = defaultComponentContext(onBackPressedDispatcher = null),
|
||||
)
|
||||
onboardingManageTokensComponent = onboardingManageTokensComponentFactory.create(
|
||||
context = componentContext,
|
||||
params = OnboardingManageTokensComponent.Params(userWalletId = userWalletId),
|
||||
)
|
||||
}
|
||||
|
||||
private fun setManageTokensState(userWalletId: UserWalletId) {
|
||||
with(binding) {
|
||||
tvHeader.show()
|
||||
tvBody.show()
|
||||
viewPagerBackupInfo.hide()
|
||||
tabLayoutBackupInfo.hide()
|
||||
onboardingWalletContainer.show()
|
||||
layoutButtonsCommon.btnWalletAlternativeAction.hide()
|
||||
onboardingWalletContainer.hide()
|
||||
toolbar.title = getText(R.string.onboarding_add_tokens)
|
||||
}
|
||||
pbBinding.pbState.show()
|
||||
bindingSeedPhrase.onboardingSeedPhraseContainer.hide()
|
||||
if (onboardingManageTokensComponent == null) {
|
||||
initializeOnboardingManageTokensComponent(userWalletId)
|
||||
}
|
||||
with(bindingManageTokens) {
|
||||
onboardingManageTokensContainer.hide()
|
||||
onboardingManageTokensContainer.show()
|
||||
onboardingManageTokensContainer.setContent {
|
||||
TangemTheme(
|
||||
isDark = isSystemInDarkTheme(),
|
||||
windowSize = rememberWindowSize(activity = requireActivity()),
|
||||
) {
|
||||
onboardingManageTokensComponent?.Content(modifier = Modifier.fillMaxSize())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupCreateWalletState() = with(binding) {
|
||||
layoutButtonsCommon.btnWalletMainAction.setText(R.string.onboarding_create_wallet_button_create_wallet)
|
||||
layoutButtonsCommon.btnWalletMainAction.setIconResource(R.drawable.ic_tangem_24)
|
||||
|
|
@ -256,7 +321,10 @@ class OnboardingWalletFragment :
|
|||
BackupStep.ReenterAccessCode -> showReenterAccessCode(state)
|
||||
is BackupStep.WritePrimaryCard -> showWritePrimaryCard(state)
|
||||
is BackupStep.WriteBackupCard -> showWriteBackupCard(state)
|
||||
BackupStep.Finished -> showSuccess()
|
||||
BackupStep.Finished -> {
|
||||
// don't need to navigate here"
|
||||
// showSuccess()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -454,6 +522,8 @@ class OnboardingWalletFragment :
|
|||
tvBody.show()
|
||||
viewPagerBackupInfo.hide()
|
||||
tabLayoutBackupInfo.hide()
|
||||
onboardingWalletContainer.show()
|
||||
bindingManageTokens.onboardingManageTokensContainer.hide()
|
||||
|
||||
tvBody.text = getText(R.string.onboarding_subtitle_success_tangem_wallet_onboarding)
|
||||
layoutButtonsCommon.btnWalletMainAction.text = getText(R.string.onboarding_button_continue_wallet)
|
||||
|
|
|
|||
|
|
@ -24,5 +24,8 @@ internal sealed interface SaveWalletAction : Action {
|
|||
data object Cancel : SaveWalletAction
|
||||
}
|
||||
|
||||
data class SaveWalletAfterBackup(val hasBackupError: Boolean) : SaveWalletAction
|
||||
data class SaveWalletAfterBackup(
|
||||
val hasBackupError: Boolean,
|
||||
val shouldNavigateToWallet: Boolean,
|
||||
) : SaveWalletAction
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ import com.tangem.tap.common.extensions.dispatchWithMain
|
|||
import com.tangem.tap.common.extensions.inject
|
||||
import com.tangem.tap.common.extensions.onUserWalletSelected
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletAction
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
import com.tangem.utils.coroutines.saveIn
|
||||
|
|
@ -44,7 +45,11 @@ internal class SaveWalletMiddleware {
|
|||
is SaveWalletAction.AllowToUseBiometrics -> allowToUseBiometrics(state)
|
||||
is SaveWalletAction.EnrollBiometrics.Enroll -> enrollBiometrics()
|
||||
is SaveWalletAction.Dismiss -> dismiss(state)
|
||||
is SaveWalletAction.SaveWalletAfterBackup -> saveWalletAfterBackup(state, action.hasBackupError)
|
||||
is SaveWalletAction.SaveWalletAfterBackup -> saveWalletAfterBackup(
|
||||
state = state,
|
||||
hasBackupError = action.hasBackupError,
|
||||
shouldNavigateToWallet = action.shouldNavigateToWallet,
|
||||
)
|
||||
is SaveWalletAction.AllowToUseBiometrics.Success,
|
||||
is SaveWalletAction.AllowToUseBiometrics.Error,
|
||||
is SaveWalletAction.ProvideBackupInfo,
|
||||
|
|
@ -55,7 +60,11 @@ internal class SaveWalletMiddleware {
|
|||
}
|
||||
}
|
||||
|
||||
private fun saveWalletAfterBackup(state: SaveWalletState, hasBackupError: Boolean) {
|
||||
private fun saveWalletAfterBackup(
|
||||
state: SaveWalletState,
|
||||
hasBackupError: Boolean,
|
||||
shouldNavigateToWallet: Boolean,
|
||||
) {
|
||||
scope.launch {
|
||||
val backupInfo = state.backupInfo ?: error("Backup info is null")
|
||||
|
||||
|
|
@ -77,8 +86,13 @@ internal class SaveWalletMiddleware {
|
|||
.doOnFailure { error ->
|
||||
Timber.e(error, "Unable to save user wallet")
|
||||
}
|
||||
.doOnSuccess { mainScope.launch { store.onUserWalletSelected(userWallet) } }
|
||||
.doOnResult { navigateToWallet() }
|
||||
.doOnSuccess {
|
||||
mainScope.launch { store.onUserWalletSelected(userWallet) }
|
||||
if (!shouldNavigateToWallet) {
|
||||
store.dispatch(OnboardingWalletAction.WalletSaved(userWallet.walletId))
|
||||
}
|
||||
}
|
||||
.doOnResult { if (shouldNavigateToWallet) navigateToWallet() }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -258,6 +258,13 @@
|
|||
android:layout_height="match_parent"
|
||||
android:visibility="gone" />
|
||||
|
||||
<include
|
||||
android:id="@+id/onboarding_manage_tokens_container"
|
||||
layout="@layout/layout_onboarding_manage_tokens"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone" />
|
||||
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
|
||||
<include
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.compose.ui.platform.ComposeView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/onboarding_manage_tokens_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
|
@ -9,6 +9,9 @@ import com.tangem.domain.wallets.models.UserWalletId
|
|||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
|
||||
typealias BackendId = String
|
||||
|
||||
class HasMissedDerivationsUseCase(
|
||||
private val derivationsRepository: DerivationsRepository,
|
||||
) {
|
||||
|
|
@ -16,7 +19,7 @@ class HasMissedDerivationsUseCase(
|
|||
/** Check if user [userWalletId] has missed derivations using map of [Network.ID] with extraDerivationPath */
|
||||
suspend operator fun invoke(
|
||||
userWalletId: UserWalletId,
|
||||
networksWithDerivationPath: Map<Network.ID, String?>,
|
||||
networksWithDerivationPath: Map<BackendId, String?>,
|
||||
): Boolean {
|
||||
return derivationsRepository.hasMissedDerivations(userWalletId, networksWithDerivationPath)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.tangem.domain.card.repository
|
|||
|
||||
import com.tangem.common.extensions.ByteArrayKey
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.domain.card.BackendId
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
|
|
@ -26,6 +27,6 @@ interface DerivationsRepository {
|
|||
/** Check if user [userWalletId] has missed derivations using map of [Network.ID] with extraDerivationPath */
|
||||
suspend fun hasMissedDerivations(
|
||||
userWalletId: UserWalletId,
|
||||
networksWithDerivationPath: Map<Network.ID, String?>,
|
||||
networksWithDerivationPath: Map<BackendId, String?>,
|
||||
): Boolean
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.tangem.domain.redux
|
||||
|
||||
import org.rekotlin.Action
|
||||
|
||||
sealed class OnboardingManageTokensAction : Action {
|
||||
data object CurrenciesSaved : OnboardingManageTokensAction()
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.features.managetokens.component
|
||||
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
|
||||
interface OnboardingManageTokensComponent : ComposableContentComponent {
|
||||
|
||||
data class Params(val userWalletId: UserWalletId)
|
||||
|
||||
interface Factory : ComponentFactory<Params, OnboardingManageTokensComponent>
|
||||
}
|
||||
|
|
@ -23,8 +23,9 @@ dependencies {
|
|||
implementation(projects.core.analytics)
|
||||
|
||||
/* Project - Domain */
|
||||
implementation(projects.domain.manageTokens)
|
||||
implementation(projects.domain.card)
|
||||
implementation(projects.domain.legacy)
|
||||
implementation(projects.domain.manageTokens)
|
||||
implementation(projects.domain.tokens)
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.wallets.models)
|
||||
|
|
@ -49,4 +50,5 @@ dependencies {
|
|||
implementation(deps.kotlin.immutable.collections)
|
||||
implementation(deps.decompose.ext.compose)
|
||||
implementation(deps.timber)
|
||||
implementation(deps.reKotlin) // need for legacy onboarding
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package com.tangem.features.managetokens.component.impl
|
||||
|
||||
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.features.managetokens.component.OnboardingManageTokensComponent
|
||||
import com.tangem.features.managetokens.model.OnboardingManageTokensModel
|
||||
import com.tangem.features.managetokens.ui.OnboardingManageTokensContent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
internal class DefaultOnboardingManageTokensComponent @AssistedInject constructor(
|
||||
@Assisted context: AppComponentContext,
|
||||
@Assisted params: OnboardingManageTokensComponent.Params,
|
||||
) : OnboardingManageTokensComponent, AppComponentContext by context {
|
||||
|
||||
private val model: OnboardingManageTokensModel = getOrCreateModel(params)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.state.collectAsStateWithLifecycle()
|
||||
|
||||
OnboardingManageTokensContent(modifier = modifier, state = state)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : OnboardingManageTokensComponent.Factory {
|
||||
override fun create(
|
||||
context: AppComponentContext,
|
||||
params: OnboardingManageTokensComponent.Params,
|
||||
): DefaultOnboardingManageTokensComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -26,7 +26,8 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||
import kotlinx.coroutines.flow.update
|
||||
|
||||
internal class PreviewManageTokensComponent(
|
||||
private val isLoading: Boolean = false,
|
||||
private val isLoading: Boolean,
|
||||
params: ManageTokensComponent.Params,
|
||||
) : ManageTokensComponent {
|
||||
|
||||
private val changedItemsIds: MutableSet<String> = mutableSetOf()
|
||||
|
|
@ -36,14 +37,21 @@ internal class PreviewManageTokensComponent(
|
|||
value = ManageTokensUM.ManageContent(
|
||||
popBack = {},
|
||||
items = items,
|
||||
topBar = ManageTokensTopBarUM.ManageContent(
|
||||
title = resourceReference(id = R.string.main_manage_tokens),
|
||||
onBackButtonClick = {},
|
||||
endButton = TopAppBarButtonUM(
|
||||
iconRes = R.drawable.ic_plus_24,
|
||||
onIconClicked = {},
|
||||
),
|
||||
),
|
||||
topBar = if (params.userWalletId != null) {
|
||||
ManageTokensTopBarUM.ManageContent(
|
||||
title = resourceReference(id = R.string.main_manage_tokens),
|
||||
onBackButtonClick = {},
|
||||
endButton = TopAppBarButtonUM(
|
||||
iconRes = R.drawable.ic_plus_24,
|
||||
onIconClicked = {},
|
||||
),
|
||||
)
|
||||
} else {
|
||||
ManageTokensTopBarUM.ReadContent(
|
||||
title = resourceReference(R.string.common_search_tokens),
|
||||
onBackButtonClick = {},
|
||||
)
|
||||
},
|
||||
search = SearchBarUM(
|
||||
placeholderText = resourceReference(R.string.manage_tokens_search_placeholder),
|
||||
query = "",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,114 @@
|
|||
package com.tangem.features.managetokens.component.preview
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.features.managetokens.component.OnboardingManageTokensComponent
|
||||
import com.tangem.features.managetokens.entity.item.CurrencyItemUM
|
||||
import com.tangem.features.managetokens.entity.item.CurrencyNetworkUM
|
||||
import com.tangem.features.managetokens.entity.managetokens.OnboardingManageTokensUM
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
import com.tangem.features.managetokens.ui.OnboardingManageTokensContent
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
internal class PreviewOnboardingManageTokensComponent(
|
||||
private val isLoading: Boolean = false,
|
||||
) : OnboardingManageTokensComponent {
|
||||
|
||||
private val state = OnboardingManageTokensUM(
|
||||
items = initItems(),
|
||||
isInitialBatchLoading = false,
|
||||
isNextBatchLoading = true,
|
||||
loadMore = { false },
|
||||
onBack = {},
|
||||
search = SearchBarUM(
|
||||
placeholderText = resourceReference(R.string.manage_tokens_search_placeholder),
|
||||
query = "",
|
||||
onQueryChange = {},
|
||||
isActive = false,
|
||||
onActiveChange = { },
|
||||
),
|
||||
actionButtonConfig = OnboardingManageTokensUM.ActionButtonConfig.Later(
|
||||
onClick = {},
|
||||
showProgress = false,
|
||||
),
|
||||
)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
OnboardingManageTokensContent(state = state, modifier = modifier)
|
||||
}
|
||||
|
||||
private fun initItems() = List(size = 30) { index ->
|
||||
if (isLoading) {
|
||||
CurrencyItemUM.Loading(index)
|
||||
} else {
|
||||
if (index < 2) {
|
||||
getCustomItem(index)
|
||||
} else {
|
||||
getBasicItem(index)
|
||||
}
|
||||
}
|
||||
}.toPersistentList()
|
||||
|
||||
private fun getCustomItem(index: Int) = CurrencyItemUM.Custom(
|
||||
id = ManagedCryptoCurrency.ID(index.toString()),
|
||||
name = "Custom token $index",
|
||||
symbol = "CT$index",
|
||||
icon = CurrencyIconState.CustomTokenIcon(
|
||||
tint = Color.White,
|
||||
background = Color.Black,
|
||||
topBadgeIconResId = R.drawable.img_eth_22,
|
||||
isGrayscale = false,
|
||||
showCustomBadge = true,
|
||||
),
|
||||
onRemoveClick = {},
|
||||
)
|
||||
|
||||
private fun getBasicItem(index: Int) = CurrencyItemUM.Basic(
|
||||
id = ManagedCryptoCurrency.ID(index.toString()),
|
||||
name = "Currency $index",
|
||||
symbol = "C$index",
|
||||
icon = CurrencyIconState.CoinIcon(
|
||||
url = null,
|
||||
fallbackResId = R.drawable.img_btc_22,
|
||||
isGrayscale = false,
|
||||
showCustomBadge = false,
|
||||
),
|
||||
networks = if (index == 2) {
|
||||
CurrencyItemUM.Basic.NetworksUM.Expanded(getCurrencyNetworks())
|
||||
} else {
|
||||
CurrencyItemUM.Basic.NetworksUM.Collapsed
|
||||
},
|
||||
onExpandClick = {},
|
||||
)
|
||||
|
||||
private fun getCurrencyNetworks() = List(size = 3) { networkIndex ->
|
||||
CurrencyNetworkUM(
|
||||
network = Network(
|
||||
id = Network.ID(networkIndex.toString()),
|
||||
backendId = networkIndex.toString(),
|
||||
name = "Network $networkIndex",
|
||||
currencySymbol = "N$networkIndex",
|
||||
derivationPath = Network.DerivationPath.Card(""),
|
||||
isTestnet = false,
|
||||
standardType = Network.StandardType.ERC20,
|
||||
hasFiatFeeRate = false,
|
||||
canHandleTokens = false,
|
||||
),
|
||||
name = "NETWORK$networkIndex",
|
||||
type = "N$networkIndex",
|
||||
iconResId = R.drawable.ic_eth_16,
|
||||
isMainNetwork = networkIndex == 0,
|
||||
isSelected = false,
|
||||
onSelectedStateChange = {},
|
||||
onLongClick = {},
|
||||
)
|
||||
}.toImmutableList()
|
||||
}
|
||||
|
|
@ -16,6 +16,12 @@ internal interface ComponentModule {
|
|||
@Singleton
|
||||
fun bindManageTokensComponentFactory(factory: DefaultManageTokensComponent.Factory): ManageTokensComponent.Factory
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindOnboardingManageTokensComponentFactory(
|
||||
factory: DefaultOnboardingManageTokensComponent.Factory,
|
||||
): OnboardingManageTokensComponent.Factory
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindAddCustomTokenComponentFactory(
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import com.tangem.core.decompose.model.Model
|
|||
import com.tangem.features.managetokens.model.CustomTokenFormModel
|
||||
import com.tangem.features.managetokens.model.CustomTokenSelectorModel
|
||||
import com.tangem.features.managetokens.model.ManageTokensModel
|
||||
import com.tangem.features.managetokens.model.OnboardingManageTokensModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -20,6 +21,11 @@ internal interface ModelModule {
|
|||
@ClassKey(ManageTokensModel::class)
|
||||
fun provideManageTokensModel(model: ManageTokensModel): Model
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(OnboardingManageTokensModel::class)
|
||||
fun provideOnboardingManageTokensModel(model: OnboardingManageTokensModel): Model
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(CustomTokenFormModel::class)
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ internal sealed class ManageTokensUM {
|
|||
abstract val isInitialBatchLoading: Boolean
|
||||
abstract val isNextBatchLoading: Boolean
|
||||
abstract val items: ImmutableList<CurrencyItemUM>
|
||||
abstract val topBar: ManageTokensTopBarUM
|
||||
abstract val topBar: ManageTokensTopBarUM?
|
||||
abstract val search: SearchBarUM
|
||||
abstract val loadMore: () -> Boolean
|
||||
abstract val scrollToTop: StateEvent<Unit>
|
||||
|
|
@ -24,7 +24,7 @@ internal sealed class ManageTokensUM {
|
|||
override val isInitialBatchLoading: Boolean,
|
||||
override val isNextBatchLoading: Boolean,
|
||||
override val items: ImmutableList<CurrencyItemUM>,
|
||||
override val topBar: ManageTokensTopBarUM,
|
||||
override val topBar: ManageTokensTopBarUM?,
|
||||
override val search: SearchBarUM,
|
||||
override val loadMore: () -> Boolean,
|
||||
override val scrollToTop: StateEvent<Unit> = consumedEvent(),
|
||||
|
|
@ -35,7 +35,7 @@ internal sealed class ManageTokensUM {
|
|||
override val isInitialBatchLoading: Boolean,
|
||||
override val isNextBatchLoading: Boolean,
|
||||
override val items: ImmutableList<CurrencyItemUM>,
|
||||
override val topBar: ManageTokensTopBarUM,
|
||||
override val topBar: ManageTokensTopBarUM?,
|
||||
override val search: SearchBarUM,
|
||||
override val loadMore: () -> Boolean,
|
||||
override val scrollToTop: StateEvent<Unit> = consumedEvent(),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
package com.tangem.features.managetokens.entity.managetokens
|
||||
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import com.tangem.core.ui.event.StateEvent
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.features.managetokens.entity.item.CurrencyItemUM
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
internal data class OnboardingManageTokensUM(
|
||||
val onBack: () -> Unit,
|
||||
val isInitialBatchLoading: Boolean,
|
||||
val isNextBatchLoading: Boolean,
|
||||
val items: ImmutableList<CurrencyItemUM>,
|
||||
val loadMore: () -> Boolean,
|
||||
val search: SearchBarUM,
|
||||
val scrollToTop: StateEvent<Unit> = consumedEvent(),
|
||||
val actionButtonConfig: ActionButtonConfig,
|
||||
) {
|
||||
sealed class ActionButtonConfig {
|
||||
abstract val onClick: () -> Unit
|
||||
abstract val showProgress: Boolean
|
||||
|
||||
data class Continue(
|
||||
override val onClick: () -> Unit,
|
||||
override val showProgress: Boolean = false,
|
||||
val showTangemIcon: Boolean,
|
||||
) : ActionButtonConfig()
|
||||
|
||||
data class Later(
|
||||
override val onClick: () -> Unit,
|
||||
override val showProgress: Boolean = false,
|
||||
) : ActionButtonConfig()
|
||||
|
||||
fun copySealed(showProgress: Boolean): ActionButtonConfig {
|
||||
return when (this) {
|
||||
is Continue -> copy(showProgress = showProgress)
|
||||
is Later -> copy(showProgress = showProgress)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -75,7 +75,7 @@ internal class ManageTokensModel @Inject constructor(
|
|||
observeSearchQueryChanges()
|
||||
|
||||
modelScope.launch {
|
||||
manageTokensListManager.launchPagination(params)
|
||||
manageTokensListManager.launchPagination(source = params.source, userWalletId = params.userWalletId)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -159,12 +159,7 @@ internal class ManageTokensModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
.sample(periodMillis = 1_000)
|
||||
.onEach { query ->
|
||||
manageTokensListManager.search(
|
||||
userWalletId = params.userWalletId,
|
||||
query = query,
|
||||
)
|
||||
}
|
||||
.onEach { query -> manageTokensListManager.search(userWalletId = params.userWalletId, query = query) }
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
|
|
@ -272,10 +267,7 @@ internal class ManageTokensModel @Inject constructor(
|
|||
if (state.isInitialBatchLoading || state.isNextBatchLoading) return false
|
||||
|
||||
modelScope.launch {
|
||||
manageTokensListManager.loadMore(
|
||||
userWalletId = params.userWalletId,
|
||||
query = state.search.query,
|
||||
)
|
||||
manageTokensListManager.loadMore(userWalletId = params.userWalletId, query = state.search.query)
|
||||
}
|
||||
|
||||
return true
|
||||
|
|
|
|||
|
|
@ -0,0 +1,297 @@
|
|||
package com.tangem.features.managetokens.model
|
||||
|
||||
import arrow.core.flatten
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.decompose.di.ComponentScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.core.ui.event.triggeredEvent
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.message.SnackbarMessage
|
||||
import com.tangem.domain.card.HasMissedDerivationsUseCase
|
||||
import com.tangem.domain.managetokens.SaveManagedTokensUseCase
|
||||
import com.tangem.domain.redux.OnboardingManageTokensAction
|
||||
import com.tangem.domain.redux.ReduxStateHolder
|
||||
import com.tangem.features.managetokens.analytics.ManageTokensAnalyticEvent
|
||||
import com.tangem.features.managetokens.component.ManageTokensSource
|
||||
import com.tangem.features.managetokens.component.OnboardingManageTokensComponent
|
||||
import com.tangem.features.managetokens.entity.item.CurrencyItemUM
|
||||
import com.tangem.features.managetokens.entity.managetokens.OnboardingManageTokensUM
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
import com.tangem.features.managetokens.utils.list.ChangedCurrencies
|
||||
import com.tangem.features.managetokens.utils.list.ManageTokensListManager
|
||||
import com.tangem.pagination.BatchFetchResult
|
||||
import com.tangem.pagination.PaginationStatus
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@ComponentScoped
|
||||
internal class OnboardingManageTokensModel @Inject constructor(
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val manageTokensListManager: ManageTokensListManager,
|
||||
private val messageSender: UiMessageSender,
|
||||
private val reduxStateHolder: ReduxStateHolder,
|
||||
private val saveManagedTokensUseCase: SaveManagedTokensUseCase,
|
||||
private val hasMissedDerivationsUseCase: HasMissedDerivationsUseCase,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
paramsContainer: ParamsContainer,
|
||||
) : Model() {
|
||||
|
||||
private val params: OnboardingManageTokensComponent.Params = paramsContainer.require()
|
||||
val state: MutableStateFlow<OnboardingManageTokensUM> = MutableStateFlow(getInitialState())
|
||||
|
||||
init {
|
||||
manageTokensListManager.uiItems
|
||||
.onEach { items -> updateItems(items) }
|
||||
.launchIn(modelScope)
|
||||
|
||||
manageTokensListManager.paginationStatus
|
||||
.onEach { status -> updatePaginationStatus(status) }
|
||||
.launchIn(modelScope)
|
||||
|
||||
manageTokensListManager.currenciesToAdd
|
||||
.onEach(::handleNewAddedCurrencies)
|
||||
.launchIn(modelScope)
|
||||
|
||||
observeSearchQueryChanges()
|
||||
|
||||
modelScope.launch {
|
||||
manageTokensListManager.launchPagination(
|
||||
source = ManageTokensSource.ONBOARDING,
|
||||
userWalletId = params.userWalletId,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getInitialState(): OnboardingManageTokensUM {
|
||||
analyticsEventHandler.send(ManageTokensAnalyticEvent.ScreenOpened(source = ManageTokensSource.ONBOARDING))
|
||||
|
||||
return OnboardingManageTokensUM(
|
||||
isInitialBatchLoading = true,
|
||||
isNextBatchLoading = false,
|
||||
items = getLoadingItems(),
|
||||
loadMore = ::loadMoreItems,
|
||||
onBack = {},
|
||||
search = SearchBarUM(
|
||||
placeholderText = resourceReference(R.string.manage_tokens_search_placeholder),
|
||||
query = "",
|
||||
onQueryChange = ::searchCurrencies,
|
||||
isActive = false,
|
||||
onActiveChange = ::toggleSearchBar,
|
||||
),
|
||||
actionButtonConfig = OnboardingManageTokensUM.ActionButtonConfig.Later(
|
||||
onClick = ::onLaterClick,
|
||||
showProgress = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
private fun observeSearchQueryChanges() {
|
||||
state
|
||||
.distinctUntilChanged { old, new ->
|
||||
// It's also used to skip search activation to avoid searching an empty query
|
||||
old.search.query == new.search.query &&
|
||||
(old.search.isActive == new.search.isActive || new.search.isActive)
|
||||
}
|
||||
.transform { state ->
|
||||
val query = state.search.query
|
||||
|
||||
if (state.search.isActive) {
|
||||
emit(query)
|
||||
}
|
||||
}
|
||||
.sample(periodMillis = 1_000)
|
||||
.onEach { query -> manageTokensListManager.search(userWalletId = params.userWalletId, query = query) }
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun updateItems(items: ImmutableList<CurrencyItemUM>) {
|
||||
val updatedState = state.updateAndGet { state -> state.copy(items = items) }
|
||||
|
||||
if (updatedState.items.isEmpty() && updatedState.search.isActive) {
|
||||
val event = ManageTokensAnalyticEvent.TokensIsNotFound(
|
||||
query = updatedState.search.query,
|
||||
source = ManageTokensSource.ONBOARDING,
|
||||
)
|
||||
analyticsEventHandler.send(event)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updatePaginationStatus(status: PaginationStatus<*>) {
|
||||
state.update { state ->
|
||||
when (status) {
|
||||
is PaginationStatus.None,
|
||||
is PaginationStatus.InitialLoading,
|
||||
-> {
|
||||
if (state.search.isActive) {
|
||||
state.copy(items = getLoadingItems())
|
||||
} else {
|
||||
state.copy(items = getLoadingItems(), isInitialBatchLoading = true)
|
||||
}
|
||||
}
|
||||
is PaginationStatus.NextBatchLoading -> state.copy(isNextBatchLoading = true)
|
||||
is PaginationStatus.InitialLoadingError -> {
|
||||
val message = SnackbarMessage(
|
||||
message = status.throwable.localizedMessage
|
||||
?.let(::stringReference)
|
||||
?: resourceReference(R.string.common_error),
|
||||
)
|
||||
messageSender.send(message)
|
||||
|
||||
state.copy(
|
||||
isInitialBatchLoading = false,
|
||||
isNextBatchLoading = false,
|
||||
)
|
||||
}
|
||||
is PaginationStatus.Paginating -> {
|
||||
(status.lastResult as? BatchFetchResult.Error)?.let { fetchError ->
|
||||
Timber.e(fetchError.throwable)
|
||||
}
|
||||
|
||||
state.copy(
|
||||
isInitialBatchLoading = false,
|
||||
isNextBatchLoading = false,
|
||||
scrollToTop = if (state.isInitialBatchLoading && state.items.isNotEmpty()) {
|
||||
triggeredEvent(
|
||||
data = Unit,
|
||||
onConsume = ::consumeScrollToTopEvent,
|
||||
)
|
||||
} else {
|
||||
state.scrollToTop
|
||||
},
|
||||
)
|
||||
}
|
||||
is PaginationStatus.EndOfPagination -> state.copy(
|
||||
isInitialBatchLoading = false,
|
||||
isNextBatchLoading = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getLoadingItems(): ImmutableList<CurrencyItemUM> {
|
||||
return List(size = 10) { index ->
|
||||
CurrencyItemUM.Loading(index)
|
||||
}.toPersistentList()
|
||||
}
|
||||
|
||||
private fun consumeScrollToTopEvent() {
|
||||
state.update { state -> state.copy(scrollToTop = consumedEvent()) }
|
||||
}
|
||||
|
||||
private suspend fun handleNewAddedCurrencies(currenciesToAdd: ChangedCurrencies) {
|
||||
if (currenciesToAdd.isEmpty()) {
|
||||
state.update { state ->
|
||||
state.copy(
|
||||
actionButtonConfig = OnboardingManageTokensUM.ActionButtonConfig.Later(onClick = ::onLaterClick),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
val hasMissedDerivations = hasMissedDerivationsUseCase.invoke(
|
||||
userWalletId = params.userWalletId,
|
||||
networksWithDerivationPath = currenciesToAdd.values
|
||||
.flatten()
|
||||
.toSet()
|
||||
.associate { it.backendId to null },
|
||||
)
|
||||
state.update { state ->
|
||||
state.copy(
|
||||
actionButtonConfig = OnboardingManageTokensUM.ActionButtonConfig.Continue(
|
||||
onClick = ::saveChanges,
|
||||
showTangemIcon = hasMissedDerivations,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadMoreItems(): Boolean {
|
||||
val state = state.value
|
||||
if (state.isInitialBatchLoading || state.isNextBatchLoading) return false
|
||||
|
||||
modelScope.launch {
|
||||
manageTokensListManager.loadMore(userWalletId = params.userWalletId, query = state.search.query)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun saveChanges() = resource(
|
||||
acquire = {
|
||||
state.update { state ->
|
||||
state.copy(actionButtonConfig = state.actionButtonConfig.copySealed(showProgress = true))
|
||||
}
|
||||
},
|
||||
release = {
|
||||
state.update { state ->
|
||||
state.copy(actionButtonConfig = state.actionButtonConfig.copySealed(showProgress = false))
|
||||
}
|
||||
},
|
||||
) {
|
||||
val event = ManageTokensAnalyticEvent.TokenAdded(
|
||||
tokensCount = manageTokensListManager.currenciesToAdd.value.values.sumOf { it.size },
|
||||
source = ManageTokensSource.ONBOARDING,
|
||||
)
|
||||
analyticsEventHandler.send(event)
|
||||
|
||||
saveManagedTokensUseCase(
|
||||
userWalletId = requireNotNull(params.userWalletId),
|
||||
currenciesToAdd = manageTokensListManager.currenciesToAdd.value,
|
||||
currenciesToRemove = manageTokensListManager.currenciesToRemove.value,
|
||||
).getOrElse {
|
||||
Timber.e(it, "Failed to save changes")
|
||||
return@resource
|
||||
}
|
||||
|
||||
reduxStateHolder.dispatch(OnboardingManageTokensAction.CurrenciesSaved)
|
||||
}
|
||||
|
||||
private fun onLaterClick() = resource(
|
||||
acquire = {
|
||||
state.update { state ->
|
||||
state.copy(actionButtonConfig = state.actionButtonConfig.copySealed(showProgress = true))
|
||||
}
|
||||
},
|
||||
release = {
|
||||
state.update { state ->
|
||||
state.copy(actionButtonConfig = state.actionButtonConfig.copySealed(showProgress = false))
|
||||
}
|
||||
},
|
||||
) {
|
||||
reduxStateHolder.dispatch(OnboardingManageTokensAction.CurrenciesSaved)
|
||||
}
|
||||
|
||||
private fun searchCurrencies(query: String) {
|
||||
state.update { state ->
|
||||
state.copy(
|
||||
search = state.search.copy(
|
||||
query = query,
|
||||
isActive = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleSearchBar(isActive: Boolean) {
|
||||
state.update { state ->
|
||||
state.copy(
|
||||
search = state.search.copy(
|
||||
isActive = isActive,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -59,7 +59,9 @@ import com.tangem.core.ui.res.LocalSnackbarHostState
|
|||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.utils.WindowInsetsZero
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.features.managetokens.component.ManageTokensComponent
|
||||
import com.tangem.features.managetokens.component.ManageTokensSource
|
||||
import com.tangem.features.managetokens.component.preview.PreviewManageTokensComponent
|
||||
import com.tangem.features.managetokens.entity.item.CurrencyItemUM
|
||||
import com.tangem.features.managetokens.entity.item.CurrencyItemUM.Basic.NetworksUM
|
||||
|
|
@ -128,19 +130,21 @@ internal fun ManageTokensScreen(state: ManageTokensUM, modifier: Modifier = Modi
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun ManageTokensTopBar(topBar: ManageTokensTopBarUM, search: SearchBarUM, modifier: Modifier = Modifier) {
|
||||
private fun ManageTokensTopBar(topBar: ManageTokensTopBarUM?, search: SearchBarUM, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier.background(TangemTheme.colors.background.primary),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16),
|
||||
) {
|
||||
TangemTopAppBar(
|
||||
title = topBar.title.resolveReference(),
|
||||
startButton = TopAppBarButtonUM.Back(topBar.onBackButtonClick),
|
||||
endButton = when (topBar) {
|
||||
is ManageTokensTopBarUM.ManageContent -> topBar.endButton
|
||||
is ManageTokensTopBarUM.ReadContent -> null
|
||||
},
|
||||
)
|
||||
if (topBar != null) {
|
||||
TangemTopAppBar(
|
||||
title = topBar.title.resolveReference(),
|
||||
startButton = TopAppBarButtonUM.Back(topBar.onBackButtonClick),
|
||||
endButton = when (topBar) {
|
||||
is ManageTokensTopBarUM.ManageContent -> topBar.endButton
|
||||
is ManageTokensTopBarUM.ReadContent -> null
|
||||
},
|
||||
)
|
||||
}
|
||||
SearchBar(
|
||||
modifier = Modifier
|
||||
.padding(bottom = TangemTheme.dimens.spacing12)
|
||||
|
|
@ -196,7 +200,7 @@ private fun Content(state: ManageTokensUM, modifier: Modifier = Modifier) {
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun Currencies(
|
||||
internal fun Currencies(
|
||||
listState: LazyListState,
|
||||
items: ImmutableList<CurrencyItemUM>,
|
||||
showLoadingItem: Boolean,
|
||||
|
|
@ -466,8 +470,24 @@ private fun Preview_ManageTokens(
|
|||
private class PreviewManageTokensComponentProvider : PreviewParameterProvider<ManageTokensComponent> {
|
||||
override val values: Sequence<ManageTokensComponent>
|
||||
get() = sequenceOf(
|
||||
PreviewManageTokensComponent(),
|
||||
PreviewManageTokensComponent(isLoading = true),
|
||||
PreviewManageTokensComponent(
|
||||
isLoading = true,
|
||||
params = ManageTokensComponent.Params(
|
||||
source = ManageTokensSource.ONBOARDING,
|
||||
userWalletId = UserWalletId("wallet_id"),
|
||||
),
|
||||
),
|
||||
PreviewManageTokensComponent(
|
||||
isLoading = false,
|
||||
params = ManageTokensComponent.Params(source = ManageTokensSource.ONBOARDING, userWalletId = null),
|
||||
),
|
||||
PreviewManageTokensComponent(
|
||||
isLoading = false,
|
||||
params = ManageTokensComponent.Params(
|
||||
source = ManageTokensSource.ONBOARDING,
|
||||
userWalletId = UserWalletId("wallet_id"),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
// endregion Preview
|
||||
|
|
@ -0,0 +1,167 @@
|
|||
package com.tangem.features.managetokens.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.FabPosition
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import com.tangem.core.ui.components.BottomFade
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.components.PrimaryButtonIconEnd
|
||||
import com.tangem.core.ui.components.SecondaryButton
|
||||
import com.tangem.core.ui.components.fields.SearchBar
|
||||
import com.tangem.core.ui.components.snackbar.TangemSnackbarHost
|
||||
import com.tangem.core.ui.event.EventEffect
|
||||
import com.tangem.core.ui.res.LocalSnackbarHostState
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.utils.WindowInsetsZero
|
||||
import com.tangem.features.managetokens.component.OnboardingManageTokensComponent
|
||||
import com.tangem.features.managetokens.component.preview.PreviewOnboardingManageTokensComponent
|
||||
import com.tangem.features.managetokens.entity.managetokens.OnboardingManageTokensUM
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
|
||||
@Composable
|
||||
internal fun OnboardingManageTokensContent(state: OnboardingManageTokensUM, modifier: Modifier = Modifier) {
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
val nestedScrollConnection = remember {
|
||||
object : NestedScrollConnection {
|
||||
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
||||
keyboardController?.hide()
|
||||
|
||||
return super.onPreScroll(available, source)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
modifier = modifier.nestedScroll(nestedScrollConnection),
|
||||
containerColor = TangemTheme.colors.background.primary,
|
||||
contentWindowInsets = WindowInsetsZero,
|
||||
topBar = {
|
||||
SearchBar(
|
||||
modifier = Modifier
|
||||
.padding(vertical = TangemTheme.dimens.spacing12, horizontal = TangemTheme.dimens.spacing16),
|
||||
state = state.search,
|
||||
)
|
||||
},
|
||||
content = { innerPadding ->
|
||||
Content(
|
||||
modifier = Modifier
|
||||
.padding(innerPadding)
|
||||
.fillMaxSize(),
|
||||
state = state,
|
||||
)
|
||||
},
|
||||
snackbarHost = {
|
||||
TangemSnackbarHost(
|
||||
modifier = Modifier.padding(all = TangemTheme.dimens.spacing16),
|
||||
hostState = LocalSnackbarHostState.current,
|
||||
)
|
||||
},
|
||||
floatingActionButtonPosition = FabPosition.Center,
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.fillMaxWidth(),
|
||||
config = state.actionButtonConfig,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Content(state: OnboardingManageTokensUM, modifier: Modifier = Modifier) {
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
Box(modifier = modifier) {
|
||||
Currencies(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
listState = listState,
|
||||
items = state.items,
|
||||
showLoadingItem = state.isNextBatchLoading,
|
||||
onLoadMore = state.loadMore,
|
||||
isEditable = true,
|
||||
)
|
||||
|
||||
BottomFade(modifier = Modifier.align(Alignment.BottomCenter))
|
||||
}
|
||||
|
||||
EventEffect(event = state.scrollToTop) {
|
||||
listState.animateScrollToItem(index = 0)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FloatingActionButton(config: OnboardingManageTokensUM.ActionButtonConfig, modifier: Modifier = Modifier) {
|
||||
when (config) {
|
||||
is OnboardingManageTokensUM.ActionButtonConfig.Continue -> ContinueButton(config = config, modifier = modifier)
|
||||
is OnboardingManageTokensUM.ActionButtonConfig.Later -> SecondaryButton(
|
||||
modifier = modifier,
|
||||
text = stringResource(id = R.string.common_later),
|
||||
showProgress = config.showProgress,
|
||||
onClick = config.onClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ContinueButton(
|
||||
config: OnboardingManageTokensUM.ActionButtonConfig.Continue,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (config.showTangemIcon) {
|
||||
PrimaryButtonIconEnd(
|
||||
modifier = modifier,
|
||||
text = stringResource(id = R.string.common_continue),
|
||||
iconResId = R.drawable.ic_tangem_24,
|
||||
showProgress = config.showProgress,
|
||||
onClick = config.onClick,
|
||||
)
|
||||
} else {
|
||||
PrimaryButton(
|
||||
modifier = modifier,
|
||||
text = stringResource(id = R.string.common_continue),
|
||||
showProgress = config.showProgress,
|
||||
onClick = config.onClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Preview(showBackground = true, widthDp = 360, heightDp = 800)
|
||||
@Preview(showBackground = true, widthDp = 360, heightDp = 800, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview_ManageTokens(
|
||||
@PreviewParameter(PreviewOnboardingManageTokensComponentProvider::class) component: OnboardingManageTokensComponent,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
component.Content(Modifier.fillMaxWidth())
|
||||
}
|
||||
}
|
||||
|
||||
private class PreviewOnboardingManageTokensComponentProvider :
|
||||
CollectionPreviewParameterProvider<PreviewOnboardingManageTokensComponent>(
|
||||
collection = listOf(
|
||||
PreviewOnboardingManageTokensComponent(isLoading = false),
|
||||
PreviewOnboardingManageTokensComponent(isLoading = true),
|
||||
),
|
||||
)
|
||||
// endregion
|
||||
|
|
@ -16,7 +16,6 @@ import com.tangem.domain.managetokens.model.*
|
|||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.features.managetokens.analytics.ManageTokensAnalyticEvent
|
||||
import com.tangem.features.managetokens.component.ManageTokensComponent
|
||||
import com.tangem.features.managetokens.component.ManageTokensSource
|
||||
import com.tangem.features.managetokens.entity.item.CurrencyItemUM
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
|
|
@ -81,9 +80,9 @@ internal class ManageTokensListManager @Inject constructor(
|
|||
.distinctUntilChanged()
|
||||
val uiItems: Flow<ImmutableList<CurrencyItemUM>> = uiManager.items
|
||||
|
||||
suspend fun launchPagination(params: ManageTokensComponent.Params) = coroutineScope {
|
||||
suspend fun launchPagination(source: ManageTokensSource, userWalletId: UserWalletId?) = coroutineScope {
|
||||
scope = this
|
||||
source = params.source
|
||||
this@ManageTokensListManager.source = source
|
||||
|
||||
val batchFlow = getManagedTokensUseCase(
|
||||
context = ManageTokensListBatchingContext(
|
||||
|
|
@ -93,13 +92,13 @@ internal class ManageTokensListManager @Inject constructor(
|
|||
)
|
||||
|
||||
batchFlow.state
|
||||
.onEach { state -> updateState(state, params.userWalletId) }
|
||||
.onEach { state -> updateState(state, userWalletId) }
|
||||
.flowOn(dispatchers.default)
|
||||
.launchIn(scope = this)
|
||||
.saveIn(jobHolder)
|
||||
|
||||
// Initial load
|
||||
reload(params.userWalletId)
|
||||
reload(userWalletId)
|
||||
}
|
||||
|
||||
suspend fun reload(userWalletId: UserWalletId?) {
|
||||
|
|
|
|||
|
|
@ -195,7 +195,7 @@ internal class MarketsPortfolioModel @Inject constructor(
|
|||
hasMissedDerivationsUseCase.invoke(
|
||||
userWalletId = selectedWalletId,
|
||||
networksWithDerivationPath = addToPortfolioData.addedNetworks[selectedWalletId].orEmpty()
|
||||
.associate { Network.ID(it.networkId) to null },
|
||||
.associate { it.networkId to null },
|
||||
)
|
||||
} else {
|
||||
false
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue